feat(frontend): add tests form expense list and items
This commit is contained in:
parent
9cb8c376dd
commit
06897a86db
@ -15,6 +15,9 @@ export default defineConfig({
|
||||
}),
|
||||
pluginTypeCheck(),
|
||||
],
|
||||
html: {
|
||||
title: 'Investments Control',
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
@ -2,4 +2,16 @@ export { useAddExpenseItem } from './hooks/useAddExpenseItem';
|
||||
export { useDeleteExpenseItem } from './hooks/useDeleteExpenseItem';
|
||||
export { useEditExpenseItem } from './hooks/useEditExpenseItem';
|
||||
|
||||
export type { AddExpenseItemSuccessResponse, AddExpenseItemBody, AddExpenseItemPath } from './api/addExpenseItem';
|
||||
export type { DeleteExpenseItemPath, DeleteExpenseItemSuccessResponse } from './api/deleteExpenseItem';
|
||||
export type { EditExpenseItemBody, EditExpenseItemPath, EditExpenseItemSuccessResponse } from './api/editExpenseItem';
|
||||
|
||||
export type { AddExpenseItemFormValues } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui';
|
||||
export { default as AddExpenseItemForm } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui';
|
||||
|
||||
export type { EditExpenseItemFormValues } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui';
|
||||
export { default as EditExpenseItemForm } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui';
|
||||
|
||||
export { default as DeleteExpenseItemForm } from './ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui';
|
||||
|
||||
export type { ExpenseListItem } from './types';
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import AddExpenseItemForm from './AddExpenseItemForm.ui';
|
||||
|
||||
import type { AddExpenseItemFormProps } from './AddExpenseItemForm.ui';
|
||||
|
||||
const renderForm = (props?: Partial<AddExpenseItemFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(<AddExpenseItemForm data-testid="add-expense-item-form" onSubmit={onSubmit} {...props} />);
|
||||
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
const descriptionInputElem = screen.getByLabelText('Описание');
|
||||
const dateInputElem = screen.getByLabelText('Дата');
|
||||
const amountInputElem = screen.getByLabelText('Сумма');
|
||||
const countInputElem = screen.getByLabelText('Количество');
|
||||
const submitButtonElem = screen.getByText('Добавить');
|
||||
|
||||
return {
|
||||
titleInputElem,
|
||||
descriptionInputElem,
|
||||
dateInputElem,
|
||||
amountInputElem,
|
||||
countInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test AddExpenseItemForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('add-expense-item-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test.each([
|
||||
['enabled', {}, (el: HTMLElement) => expect(el).toBeEnabled()],
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => expect(el).toBeDisabled()],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
const { onSubmit, ...formElems } = renderForm(options);
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
descriptionInputElem,
|
||||
dateInputElem,
|
||||
amountInputElem,
|
||||
countInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.type(titleInputElem, 'Название элемента');
|
||||
await userEvent.type(descriptionInputElem, 'Описание элемента');
|
||||
await userEvent.type(dateInputElem, '2025-08-29');
|
||||
await userEvent.type(amountInputElem, '12');
|
||||
await userEvent.type(countInputElem, '1');
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
title: 'Название элемента',
|
||||
description: 'Описание элемента',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
amount: 12,
|
||||
count: 1,
|
||||
currency: 'RUB',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,110 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
|
||||
import classes from './AddExpenseItemForm.module.css';
|
||||
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
export type AddExpenseItemFormValues = {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
count: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type AddExpenseItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit?: (data: AddExpenseItemFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
const {
|
||||
disabled,
|
||||
onSubmit,
|
||||
ref,
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const title = formData.get('title');
|
||||
const date = formData.get('date');
|
||||
const amount = formData.get('amount');
|
||||
const description = formData.get('description');
|
||||
const count = formData.get('count');
|
||||
|
||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
||||
onSubmit?.({
|
||||
title,
|
||||
date: new Date(date).toISOString(),
|
||||
amount: Number(amount),
|
||||
description,
|
||||
currency: 'RUB',
|
||||
count: Number(count),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
className={classes.form}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
{...formProps}
|
||||
>
|
||||
<h2>Добавить список расходов</h2>
|
||||
|
||||
<Input
|
||||
required
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="title"
|
||||
/>
|
||||
|
||||
<Input
|
||||
disabled={disabled}
|
||||
label="Описание"
|
||||
name="description"
|
||||
/>
|
||||
|
||||
<Input
|
||||
required
|
||||
disabled={disabled}
|
||||
label="Дата"
|
||||
name="date"
|
||||
type="date"
|
||||
/>
|
||||
|
||||
<Input
|
||||
required
|
||||
disabled={disabled}
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Input
|
||||
disabled={disabled}
|
||||
label="Количество"
|
||||
name="count"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<button disabled={disabled} type="submit">Добавить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddExpenseItemForm;
|
||||
@ -0,0 +1,60 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import DeleteExpenseItemForm from './DeleteExpenseItemForm.ui';
|
||||
|
||||
import type { DeleteExpenseItemFormProps } from './DeleteExpenseItemForm.ui';
|
||||
|
||||
const deletingItem = {
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
title: 'Mock item',
|
||||
amount: 12,
|
||||
count: 12,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
};
|
||||
|
||||
const renderForm = (props?: Partial<DeleteExpenseItemFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
<DeleteExpenseItemForm
|
||||
data-testid="delete-expense-item-form"
|
||||
item={deletingItem}
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
const submitButtonElem = screen.getByText('Удалить');
|
||||
|
||||
return {
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test DeleteExpenseItemForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('delete-expense-item-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith(deletingItem);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,40 @@
|
||||
import type { ExpenseListItem } from '../../types';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type DeleteExpenseItemFormProps = BaseFormProps & {
|
||||
item: ExpenseListItem;
|
||||
onSubmit?: (item: ExpenseListItem) => Promise<void> | void;
|
||||
disabled?: boolean;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
const DeleteExpenseItemForm: FC<DeleteExpenseItemFormProps> = (props) => {
|
||||
const {
|
||||
item,
|
||||
onSubmit,
|
||||
ref,
|
||||
disabled,
|
||||
...restProps
|
||||
} = props;
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
onSubmit?.(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<form ref={ref} onSubmit={onFormSubmitHandler} {...restProps}>
|
||||
<h2>Удалить элемент ?</h2>
|
||||
|
||||
<button disabled={disabled} type="submit">Удалить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteExpenseItemForm;
|
||||
@ -0,0 +1,120 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import EditExpenseItemForm from './EditExpenseItemForm.ui';
|
||||
|
||||
import type { EditExpenseItemFormProps } from './EditExpenseItemForm.ui';
|
||||
|
||||
const editingItem = {
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
title: 'Mock item',
|
||||
amount: 12,
|
||||
count: 12,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
};
|
||||
|
||||
const renderForm = (props?: Partial<EditExpenseItemFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
<EditExpenseItemForm
|
||||
data-testid="edit-expense-item-form"
|
||||
item={editingItem}
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
const descriptionInputElem = screen.getByLabelText('Описание');
|
||||
const dateInputElem = screen.getByLabelText('Дата');
|
||||
const amountInputElem = screen.getByLabelText('Сумма');
|
||||
const countInputElem = screen.getByLabelText('Количество');
|
||||
const submitButtonElem = screen.getByText('Сохранить');
|
||||
|
||||
return {
|
||||
titleInputElem,
|
||||
descriptionInputElem,
|
||||
dateInputElem,
|
||||
amountInputElem,
|
||||
countInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test EditExpenseItemForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('edit-expense-item-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test('should inputs have default value', () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
descriptionInputElem,
|
||||
dateInputElem,
|
||||
amountInputElem,
|
||||
countInputElem,
|
||||
} = renderForm();
|
||||
expect(titleInputElem).toHaveValue('Mock item');
|
||||
expect(descriptionInputElem).toHaveValue('Mock description');
|
||||
expect(dateInputElem).toHaveValue('2025-08-22');
|
||||
expect(amountInputElem).toHaveValue(12);
|
||||
expect(countInputElem).toHaveValue(12);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['enabled', {}, (el: HTMLElement) => expect(el).toBeEnabled()],
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => expect(el).toBeDisabled()],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
const { onSubmit, ...formElems } = renderForm(options);
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
descriptionInputElem,
|
||||
dateInputElem,
|
||||
amountInputElem,
|
||||
countInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.clear(titleInputElem);
|
||||
await userEvent.type(titleInputElem, 'Название элемента');
|
||||
|
||||
await userEvent.clear(descriptionInputElem);
|
||||
await userEvent.type(descriptionInputElem, 'Описание элемента');
|
||||
|
||||
await userEvent.clear(dateInputElem);
|
||||
await userEvent.type(dateInputElem, '2025-08-29');
|
||||
|
||||
await userEvent.clear(amountInputElem);
|
||||
await userEvent.type(amountInputElem, '12');
|
||||
|
||||
await userEvent.clear(countInputElem);
|
||||
await userEvent.type(countInputElem, '1');
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
title: 'Название элемента',
|
||||
description: 'Описание элемента',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
amount: 12,
|
||||
count: 1,
|
||||
currency: 'RUB',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,113 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
|
||||
import classes from './EditExpenseItemForm.module.css';
|
||||
|
||||
import type { ExpenseListItem } from '../../types';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
export type EditExpenseItemFormValues = {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
count: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
export type EditExpenseItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit?: (data: EditExpenseItemFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
item: ExpenseListItem;
|
||||
};
|
||||
|
||||
const EditExpenseItemForm: FC<EditExpenseItemFormProps> = (props) => {
|
||||
const {
|
||||
disabled,
|
||||
onSubmit,
|
||||
ref,
|
||||
item,
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const title = formData.get('title');
|
||||
const date = formData.get('date');
|
||||
const amount = formData.get('amount');
|
||||
const description = formData.get('description');
|
||||
const count = formData.get('count');
|
||||
|
||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
||||
await onSubmit?.({
|
||||
title,
|
||||
date: new Date(date).toISOString(),
|
||||
amount: Number(amount),
|
||||
description,
|
||||
currency: 'RUB',
|
||||
count: Number(count),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
className={classes.form}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
{...formProps}
|
||||
>
|
||||
<h2>Добавить список расходов</h2>
|
||||
|
||||
<Input
|
||||
defaultValue={item.title}
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="title"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={item.description ?? ''}
|
||||
disabled={disabled}
|
||||
label="Описание"
|
||||
name="description"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={new Date(item.date).toISOString().split('T')[0]}
|
||||
disabled={disabled}
|
||||
label="Дата"
|
||||
name="date"
|
||||
type="date"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={item.amount}
|
||||
disabled={disabled}
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={item.count ?? 1}
|
||||
disabled={disabled}
|
||||
label="Количество"
|
||||
name="count"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<button disabled={disabled} type="submit">Сохранить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditExpenseItemForm;
|
||||
@ -2,8 +2,9 @@ import { api } from '@/shared/api/client';
|
||||
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
type AddExpenseListBody = paths['/expenses/list']['post']['requestBody']['content']['application/json'];
|
||||
type AddExpenseListSuccessResponse = paths['/expenses/list']['post']['responses']['200']['content']['application/json'];
|
||||
export type AddExpenseListPath = paths['/expenses/list']['post']['parameters']['path'];
|
||||
export type AddExpenseListBody = paths['/expenses/list']['post']['requestBody']['content']['application/json'];
|
||||
export type AddExpenseListSuccessResponse = paths['/expenses/list']['post']['responses']['200']['content']['application/json'];
|
||||
|
||||
export async function addExpensesList(data: AddExpenseListBody) {
|
||||
const { data: result } = await api.post<AddExpenseListSuccessResponse>('/expenses/list', data);
|
||||
|
||||
@ -2,8 +2,9 @@ import { api } from '@/shared/api/client';
|
||||
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
type DeleteExpenseListPath = paths['/expenses/list/{id}']['delete']['parameters']['path'];
|
||||
type DeleteExpenseSuccessResponse = paths['/expenses/list/{id}']['delete']['responses']['200']['content']['application/json'];
|
||||
export type DeleteExpenseListPath = paths['/expenses/list/{id}']['delete']['parameters']['path'];
|
||||
export type DeleteExpenseSuccessResponse = paths['/expenses/list/{id}']['delete']['responses']['200']['content']['application/json'];
|
||||
|
||||
export async function deleteExpensesList(data: DeleteExpenseListPath) {
|
||||
const { id } = data;
|
||||
const { data: result } = await api.delete<DeleteExpenseSuccessResponse>(`/expenses/list/${id}`);
|
||||
|
||||
@ -2,9 +2,9 @@ import { api } from '@/shared/api/client';
|
||||
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
type EditExpenseListPath = paths['/expenses/list/{id}']['patch']['parameters']['path'];
|
||||
type EditExpenseListBody = paths['/expenses/list/{id}']['patch']['requestBody']['content']['application/json'];
|
||||
type EditExpenseListSuccessResponse = paths['/expenses/list/{id}']['patch']['responses']['200']['content']['application/json'];
|
||||
export type EditExpenseListPath = paths['/expenses/list/{id}']['patch']['parameters']['path'];
|
||||
export type EditExpenseListBody = paths['/expenses/list/{id}']['patch']['requestBody']['content']['application/json'];
|
||||
export type EditExpenseListSuccessResponse = paths['/expenses/list/{id}']['patch']['responses']['200']['content']['application/json'];
|
||||
|
||||
export async function editExpensesList({ data, path }: {
|
||||
path: EditExpenseListPath;
|
||||
|
||||
@ -5,3 +5,15 @@ export { useExpensesList } from './hooks/useExpensesList';
|
||||
export { useEditExpensesList } from './hooks/useEditExpensesList';
|
||||
|
||||
export { useExpenseList } from './hooks/useExpenseList';
|
||||
|
||||
export type { AddExpenseListFormValues } from './ui/AddExpensesListForm/ui/AddExpensesListForm.ui';
|
||||
export { default as AddExpensesListForm } from './ui/AddExpensesListForm/ui/AddExpensesListForm.ui';
|
||||
|
||||
export { default as DeleteExpensesListForm } from './ui/DeleteExpensesListForm/ui/DeleteExpensesListForm.ui';
|
||||
|
||||
export type { EditExpenseListFormValues } from './ui/EditExpensesListForm/ui/EditExpensesListForm.ui';
|
||||
export { default as EditExpensesListForm } from './ui/EditExpensesListForm/ui/EditExpensesListForm.ui';
|
||||
|
||||
export type { AddExpenseListPath, AddExpenseListBody, AddExpenseListSuccessResponse } from './api/addExpensesList';
|
||||
export type { DeleteExpenseListPath, DeleteExpenseSuccessResponse } from './api/deleteExpensesList';
|
||||
export type { EditExpenseListPath, EditExpenseListBody, EditExpenseListSuccessResponse } from './api/editExpensesList';
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import AddExpensesListForm from './AddExpensesListForm.ui';
|
||||
|
||||
import type { AddExpenseListFormProps } from './AddExpensesListForm.ui';
|
||||
|
||||
const renderForm = (props?: Partial<AddExpenseListFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(<AddExpensesListForm data-testid="add-expense-list-form" onSubmit={onSubmit} {...props} />);
|
||||
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
const submitButtonElem = screen.getByText('Добавить');
|
||||
|
||||
return {
|
||||
titleInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test AddExpensesListForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('add-expense-list-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test.each([
|
||||
['enabled', {}, (el: HTMLElement) => expect(el).toBeEnabled()],
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => expect(el).toBeDisabled()],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
const { onSubmit, ...formElems } = renderForm(options);
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.type(titleInputElem, 'Название списка');
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
title: 'Название списка',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
|
||||
import classes from './AddExpenseListForm.module.css';
|
||||
|
||||
import type {
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
FormEventHandler,
|
||||
FC,
|
||||
} from 'react';
|
||||
|
||||
export type AddExpenseListFormValues = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type AddExpenseListFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: AddExpenseListFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
const AddExpensesListForm: FC<AddExpenseListFormProps> = (props) => {
|
||||
const {
|
||||
ref,
|
||||
disabled,
|
||||
onSubmit,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const title = formData.get('title');
|
||||
|
||||
if (typeof title === 'string') {
|
||||
onSubmit?.({
|
||||
title,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
className={classes.form}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
{...restProps}
|
||||
>
|
||||
<h2>Добавить список расходов</h2>
|
||||
|
||||
<Input disabled={disabled} label="Название" name="title" />
|
||||
|
||||
<button disabled={disabled} type="submit">Добавить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddExpensesListForm;
|
||||
@ -0,0 +1,55 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import DeleteExpensesListForm from './DeleteExpensesListForm.ui';
|
||||
|
||||
import type { DeleteExpenseListFormProps } from './DeleteExpensesListForm.ui';
|
||||
|
||||
const deletingList = {
|
||||
id: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
title: 'Mock list',
|
||||
items: [],
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const renderForm = (props?: Partial<DeleteExpenseListFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
<DeleteExpensesListForm
|
||||
data-testid="delete-expense-list-form"
|
||||
list={deletingList}
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
const submitButtonElem = screen.getByText('Удалить');
|
||||
|
||||
return {
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test DeleteExpensesListForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('delete-expense-list-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith(deletingList);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,64 @@
|
||||
import type { ExpenseList } from '@/entity/expenses/list';
|
||||
import type {
|
||||
FC, FormEventHandler, FormHTMLAttributes, RefObject,
|
||||
} from 'react';
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type DeleteExpenseListFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit?: (list: ExpenseList) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
list: ExpenseList;
|
||||
};
|
||||
|
||||
const DeleteExpensesListForm: FC<DeleteExpenseListFormProps> = (props) => {
|
||||
const {
|
||||
ref,
|
||||
disabled,
|
||||
onSubmit,
|
||||
list,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
onSubmit?.(list);
|
||||
};
|
||||
|
||||
return (
|
||||
<form ref={ref} onSubmit={onFormSubmitHandler} {...restProps}>
|
||||
<h2>Удалить список расходов</h2>
|
||||
|
||||
<p>
|
||||
Вы действительно хотите удалить список расходов
|
||||
{' '}
|
||||
|
||||
<b>{`"${list.title}"`}</b>
|
||||
|
||||
{' '}
|
||||
?
|
||||
</p>
|
||||
|
||||
{
|
||||
list?.items && list?.items?.length > 0
|
||||
? (
|
||||
<p>
|
||||
В нем содержится
|
||||
{' '}
|
||||
|
||||
<b>{list.items?.length}</b>
|
||||
|
||||
{' '}
|
||||
расходов
|
||||
</p>
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
<button disabled={disabled} type="submit">Удалить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteExpensesListForm;
|
||||
@ -0,0 +1,5 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import EditExpensesListForm from './EditExpensesListForm.ui';
|
||||
|
||||
import type { EditExpensesListFormProps } from './EditExpensesListForm.ui';
|
||||
|
||||
const editingList = {
|
||||
id: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
title: 'Mock list',
|
||||
items: [],
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const renderForm = (props?: Partial<EditExpensesListFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
<EditExpensesListForm
|
||||
data-testid="edit-expense-list-form"
|
||||
list={editingList}
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
const submitButtonElem = screen.getByText('Сохранить');
|
||||
|
||||
return {
|
||||
titleInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test EditExpensesListForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('edit-expense-list-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test('should inputs have default value', () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
} = renderForm();
|
||||
expect(titleInputElem).toHaveValue('Mock list');
|
||||
});
|
||||
|
||||
test.each([
|
||||
['enabled', {}, (el: HTMLElement) => expect(el).toBeEnabled()],
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => expect(el).toBeDisabled()],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
const { onSubmit, ...formElems } = renderForm(options);
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.clear(titleInputElem);
|
||||
await userEvent.type(titleInputElem, 'Название списка');
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
title: 'Название списка',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,68 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
|
||||
import classes from './EditExpenseListForm.module.css';
|
||||
|
||||
import type { ExpenseList } from '@/entity/expenses/list';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
export type EditExpenseListFormValues = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type EditExpensesListFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: EditExpenseListFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
list: ExpenseList;
|
||||
};
|
||||
|
||||
const EditExpensesListForm: FC<EditExpensesListFormProps> = (props) => {
|
||||
const {
|
||||
ref,
|
||||
disabled,
|
||||
onSubmit,
|
||||
list,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const title = formData.get('title');
|
||||
|
||||
if (typeof title === 'string') {
|
||||
onSubmit({
|
||||
title,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
className={classes.form}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
{...restProps}
|
||||
>
|
||||
<h2>Редактировать список расходов</h2>
|
||||
|
||||
<Input
|
||||
defaultValue={list.title}
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="title"
|
||||
/>
|
||||
|
||||
<button disabled={disabled} type="submit">Сохранить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditExpensesListForm;
|
||||
@ -1,42 +1,32 @@
|
||||
import { type FormEventHandler, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useAddExpenseItem } from '@/entity/expenses/item';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '@/entity/expenses/item';
|
||||
import type { ExpenseList } from '@/entity/expenses/list';
|
||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||
|
||||
type UseAddExpenseItemButtonArgs = {
|
||||
listId: ExpenseList['id'];
|
||||
onClick?: ButtonHTMLAttributes<HTMLButtonElement>['onClick'];
|
||||
};
|
||||
export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
||||
const { listId } = args;
|
||||
const { listId, onClick } = args;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync: addExpenseItemMutation, isPending } = useAddExpenseItem({ id: listId });
|
||||
const onCloseModalHandler = () => setIsOpen(false);
|
||||
|
||||
const onClickHandler = () => setIsOpen(true);
|
||||
const onClickHandler: MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||
setIsOpen(true);
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
const onFormSubmitHandler = async (data: AddExpenseItemFormValues) => {
|
||||
// todo добавить обработку ошибок (код + тест)
|
||||
await addExpenseItemMutation(data);
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const title = formData.get('title');
|
||||
const date = formData.get('date');
|
||||
const amount = formData.get('amount');
|
||||
const description = formData.get('description');
|
||||
const count = formData.get('count');
|
||||
|
||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
||||
await addExpenseItemMutation({
|
||||
title,
|
||||
date: new Date(date).toISOString(),
|
||||
amount: Number(amount),
|
||||
description,
|
||||
currency: 'RUB',
|
||||
count: Number(count),
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import AddExpenseItemButton from './AddExpenseItemButton.ui';
|
||||
|
||||
import type { AddExpenseItemBody, AddExpenseItemPath, AddExpenseItemSuccessResponse } from '@/entity/expenses/item';
|
||||
|
||||
const renderButton = () => {
|
||||
render(
|
||||
<AddExpenseItemButton data-testid="test-button" listId="123" />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId('test-button');
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const fillForm = async (data: { title: string; description: string; date: string; amount: string; count: string }) => {
|
||||
await userEvent.type(screen.getByLabelText('Название'), data.title);
|
||||
await userEvent.type(screen.getByLabelText('Описание'), data.description);
|
||||
await userEvent.type(screen.getByLabelText('Дата'), data.date);
|
||||
await userEvent.type(screen.getByLabelText('Сумма'), data.amount);
|
||||
await userEvent.type(screen.getByLabelText('Количество'), data.count);
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.post<
|
||||
AddExpenseItemPath,
|
||||
AddExpenseItemBody,
|
||||
AddExpenseItemSuccessResponse
|
||||
>(
|
||||
'http://localhost:3000/expenses/list/123/items',
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json({
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
title: 'Mock item',
|
||||
amount: 12,
|
||||
count: 12,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('Test AddExpenseItemButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Добавить элемент');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
let submitButtonElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(submitHandler(requestSpy));
|
||||
modalElem = (await openModal()).modalElem;
|
||||
submitButtonElem = screen.getByText('Добавить');
|
||||
|
||||
await fillForm({
|
||||
title: 'Название элемента',
|
||||
description: 'Описание элемента',
|
||||
date: '2025-08-29',
|
||||
amount: '12',
|
||||
count: '1',
|
||||
});
|
||||
});
|
||||
|
||||
test('should send correct request', async () => {
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
title: 'Название элемента',
|
||||
description: 'Описание элемента',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
amount: 12,
|
||||
count: 1,
|
||||
currency: 'RUB',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,19 +1,25 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
import { AddExpenseItemForm } from '@/entity/expenses/item';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useAddExpenseItemButton } from '../hooks/useAddExpenseItemButton';
|
||||
|
||||
import classes from './AddExpenseItemButton.module.css';
|
||||
|
||||
import type { ExpenseList } from '@/entity/expenses/list';
|
||||
import type { FC } from 'react';
|
||||
import type { ButtonHTMLAttributes, FC, RefObject } from 'react';
|
||||
|
||||
type AddExpenseItemButtonProps = {
|
||||
type BaseButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
type AddExpenseItemButtonProps = BaseButtonProps & {
|
||||
listId: ExpenseList['id'];
|
||||
ref?: RefObject<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
const AddExpenseItemButton: FC<AddExpenseItemButtonProps> = (props) => {
|
||||
const { listId } = props;
|
||||
const {
|
||||
listId,
|
||||
ref,
|
||||
onClick,
|
||||
...buttonProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@ -21,56 +27,21 @@ const AddExpenseItemButton: FC<AddExpenseItemButtonProps> = (props) => {
|
||||
onClickHandler,
|
||||
onCloseModalHandler,
|
||||
onFormSubmitHandler,
|
||||
} = useAddExpenseItemButton({ listId });
|
||||
} = useAddExpenseItemButton({ listId, onClick });
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={onClickHandler}>
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={onClickHandler}
|
||||
{...buttonProps}
|
||||
>
|
||||
Добавить элемент
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<form className={classes.form} onSubmit={onFormSubmitHandler}>
|
||||
<h2>Добавить список расходов</h2>
|
||||
|
||||
<Input
|
||||
required
|
||||
disabled={isPending}
|
||||
label="Название"
|
||||
name="title"
|
||||
/>
|
||||
|
||||
<Input
|
||||
disabled={isPending}
|
||||
label="Описание"
|
||||
name="description"
|
||||
/>
|
||||
|
||||
<Input
|
||||
required
|
||||
disabled={isPending}
|
||||
label="Дата"
|
||||
name="date"
|
||||
type="date"
|
||||
/>
|
||||
|
||||
<Input
|
||||
required
|
||||
disabled={isPending}
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Input
|
||||
disabled={isPending}
|
||||
label="Количество"
|
||||
name="count"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<button disabled={isPending} type="submit">Добавить</button>
|
||||
</form>
|
||||
<AddExpenseItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
|
||||
@ -1,28 +1,32 @@
|
||||
import { type FormEventHandler, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type ExpenseListItem, useDeleteExpenseItem } from '@/entity/expenses/item';
|
||||
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
type UseDeleteExpenseItemButtonArgs = {
|
||||
item: ExpenseListItem;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export const useDeleteExpenseItemButton = (args: UseDeleteExpenseItemButtonArgs) => {
|
||||
const { item } = args;
|
||||
const { item, onClick } = args;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onCloseModalHandler = () => setIsOpen(false);
|
||||
const onClickHandler = () => setIsOpen(true);
|
||||
const onClickHandler: MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||
setIsOpen(true);
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
const { mutateAsync: deleteExpenseItemMutation, isPending } = useDeleteExpenseItem({
|
||||
listId: item.expenseListId,
|
||||
id: item.id,
|
||||
});
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
await deleteExpenseItemMutation();
|
||||
setIsOpen(false);
|
||||
const onFormSubmitHandler = async () => {
|
||||
await deleteExpenseItemMutation();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import DeleteExpenseItemButton from './DeleteExpenseItemButton.ui';
|
||||
|
||||
import type { DeleteExpenseItemPath } from '@/entity/expenses/item';
|
||||
|
||||
const deletedItem = {
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
title: 'Mock item',
|
||||
amount: 12,
|
||||
count: 12,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
render(
|
||||
<DeleteExpenseItemButton data-testid="delete-expense-item-button" item={deletedItem} />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId('delete-expense-item-button');
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = http.delete<DeleteExpenseItemPath>(`http://localhost:3000/expenses/list/${deletedItem.expenseListId}/items/${deletedItem.id}`, () => HttpResponse.json(deletedItem));
|
||||
|
||||
describe('Test DeleteExpenseItemButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Удалить');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
server.use(submitHandler);
|
||||
const { modalElem } = await openModal();
|
||||
const submitButtonElem = within(modalElem).getByText('Удалить');
|
||||
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -1,35 +1,45 @@
|
||||
import { DeleteExpenseItemForm } from '@/entity/expenses/item';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useDeleteExpenseItemButton } from '../hooks/useDeleteExpenseItemButton';
|
||||
|
||||
import type { ExpenseListItem } from '@/entity/expenses/item';
|
||||
import type { FC } from 'react';
|
||||
import type { ButtonHTMLAttributes, FC, RefObject } from 'react';
|
||||
|
||||
type DeleteExpenseItemButtonProps = {
|
||||
type BaseButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
type DeleteExpenseItemButtonProps = BaseButtonProps & {
|
||||
item: ExpenseListItem;
|
||||
ref?: RefObject<HTMLButtonElement>;
|
||||
};
|
||||
const DeleteExpenseItemButton: FC<DeleteExpenseItemButtonProps> = (props) => {
|
||||
const { item } = props;
|
||||
const {
|
||||
item,
|
||||
ref,
|
||||
onClick,
|
||||
...restProps
|
||||
} = props;
|
||||
const {
|
||||
isOpen,
|
||||
onCloseModalHandler,
|
||||
onClickHandler,
|
||||
isPending,
|
||||
onFormSubmitHandler,
|
||||
} = useDeleteExpenseItemButton({ item });
|
||||
} = useDeleteExpenseItemButton({ item, onClick });
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={onClickHandler}>
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={onClickHandler}
|
||||
{...restProps}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<form onSubmit={onFormSubmitHandler}>
|
||||
<h2>Удалить элемент ?</h2>
|
||||
|
||||
<button disabled={isPending} type="submit">Удалить</button>
|
||||
</form>
|
||||
<DeleteExpenseItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import { type FormEventHandler, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type ExpenseListItem, useEditExpenseItem } from '@/entity/expenses/item';
|
||||
import { type ExpenseListItem, useEditExpenseItem, type EditExpenseItemFormValues } from '@/entity/expenses/item';
|
||||
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
type UseAddExpenseItemButtonArgs = {
|
||||
item: ExpenseListItem;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
||||
const { item } = args;
|
||||
|
||||
export const useEditExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
||||
const { item, onClick } = args;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync: editExpenseItemMutation, isPending } = useEditExpenseItem({
|
||||
@ -15,29 +19,15 @@ export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
||||
});
|
||||
const onCloseModalHandler = () => setIsOpen(false);
|
||||
|
||||
const onClickHandler = () => setIsOpen(true);
|
||||
const onClickHandler: MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||
setIsOpen(true);
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const title = formData.get('title');
|
||||
const date = formData.get('date');
|
||||
const amount = formData.get('amount');
|
||||
const description = formData.get('description');
|
||||
const count = formData.get('count');
|
||||
|
||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
||||
await editExpenseItemMutation({
|
||||
title,
|
||||
date: new Date(date).toISOString(),
|
||||
amount: Number(amount),
|
||||
description,
|
||||
currency: 'RUB',
|
||||
count: Number(count),
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
const onFormSubmitHandler = async (data: EditExpenseItemFormValues) => {
|
||||
// todo добавить обработку ошибок (код + тест)
|
||||
await editExpenseItemMutation(data);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import EditExpenseItemButton from './EditExpenseItemButton.ui';
|
||||
|
||||
import type {
|
||||
EditExpenseItemBody,
|
||||
EditExpenseItemPath,
|
||||
EditExpenseItemSuccessResponse,
|
||||
} from '@/entity/expenses/item';
|
||||
|
||||
const editingItem = {
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
title: 'Mock item',
|
||||
amount: 12,
|
||||
count: 12,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
render(
|
||||
<EditExpenseItemButton data-testid="edit-expense-item-button" item={editingItem} />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId('edit-expense-item-button');
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.patch<
|
||||
EditExpenseItemPath,
|
||||
EditExpenseItemBody,
|
||||
EditExpenseItemSuccessResponse
|
||||
>(
|
||||
`http://localhost:3000/expenses/list/${editingItem.expenseListId}/items/${editingItem.id}`,
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json(editingItem);
|
||||
},
|
||||
);
|
||||
|
||||
const fillForm = async (data: { title: string; description: string; date: string; amount: string; count: string }) => {
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
await userEvent.clear(titleInputElem);
|
||||
await userEvent.type(titleInputElem, data.title);
|
||||
|
||||
const descriptionInputElem = screen.getByLabelText('Описание');
|
||||
await userEvent.clear(descriptionInputElem);
|
||||
await userEvent.type(descriptionInputElem, data.description);
|
||||
|
||||
const dateInputElem = screen.getByLabelText('Дата');
|
||||
await userEvent.clear(dateInputElem);
|
||||
await userEvent.type(dateInputElem, data.date);
|
||||
|
||||
const amountInputElem = screen.getByLabelText('Сумма');
|
||||
await userEvent.clear(amountInputElem);
|
||||
await userEvent.type(amountInputElem, data.amount);
|
||||
|
||||
const countInputElem = screen.getByLabelText('Количество');
|
||||
await userEvent.clear(countInputElem);
|
||||
await userEvent.type(countInputElem, data.count);
|
||||
};
|
||||
|
||||
describe('TestEditExpenseItemButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Редактировать');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
let submitButtonElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(submitHandler(requestSpy));
|
||||
modalElem = (await openModal()).modalElem;
|
||||
submitButtonElem = screen.getByText('Сохранить');
|
||||
|
||||
await fillForm({
|
||||
title: 'Название элемента',
|
||||
description: 'Описание элемента',
|
||||
date: '2025-08-29',
|
||||
amount: '12',
|
||||
count: '1',
|
||||
});
|
||||
});
|
||||
|
||||
test('should send correct request', async () => {
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
title: 'Название элемента',
|
||||
description: 'Описание элемента',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
amount: 12,
|
||||
count: 1,
|
||||
currency: 'RUB',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,19 +1,25 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
import { EditExpenseItemForm } from '@/entity/expenses/item';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useAddExpenseItemButton } from '../hooks/useEditExpenseItemButton';
|
||||
|
||||
import classes from './EditExpenseItemButton.module.css';
|
||||
import { useEditExpenseItemButton } from '../hooks/useEditExpenseItemButton';
|
||||
|
||||
import type { ExpenseListItem } from '@/entity/expenses/item';
|
||||
import type { FC } from 'react';
|
||||
import type { ButtonHTMLAttributes, FC, RefObject } from 'react';
|
||||
|
||||
type EditExpenseItemButtonProps = {
|
||||
type BaseButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
type EditExpenseItemButtonProps = BaseButtonProps & {
|
||||
item: ExpenseListItem;
|
||||
ref?: RefObject<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
const EditExpenseItemButton: FC<EditExpenseItemButtonProps> = (props) => {
|
||||
const { item } = props;
|
||||
const {
|
||||
ref,
|
||||
item,
|
||||
onClick,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@ -21,58 +27,22 @@ const EditExpenseItemButton: FC<EditExpenseItemButtonProps> = (props) => {
|
||||
onClickHandler,
|
||||
isPending,
|
||||
onFormSubmitHandler,
|
||||
} = useAddExpenseItemButton({ item });
|
||||
} = useEditExpenseItemButton({ item, onClick });
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={onClickHandler}>
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={isPending}
|
||||
type="button"
|
||||
onClick={onClickHandler}
|
||||
{...restProps}
|
||||
>
|
||||
Редактировать
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<form className={classes.form} onSubmit={onFormSubmitHandler}>
|
||||
<h2>Добавить список расходов</h2>
|
||||
|
||||
<Input
|
||||
defaultValue={item.title}
|
||||
disabled={isPending}
|
||||
label="Название"
|
||||
name="title"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={item.description ?? ''}
|
||||
disabled={isPending}
|
||||
label="Описание"
|
||||
name="description"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={new Date(item.date).toISOString().split('T')[0]}
|
||||
disabled={isPending}
|
||||
label="Дата"
|
||||
name="date"
|
||||
type="date"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={item.amount}
|
||||
disabled={isPending}
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={item.count ?? 1}
|
||||
disabled={isPending}
|
||||
label="Количество"
|
||||
name="count"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<button disabled={isPending} type="submit">Сохранить</button>
|
||||
</form>
|
||||
<EditExpenseItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { type FormEventHandler, type MouseEventHandler, useState } from 'react';
|
||||
import { type MouseEventHandler, useState } from 'react';
|
||||
|
||||
import { useAddExpensesList } from '@/entity/expenses/list';
|
||||
|
||||
import type { AddExpenseListFormValues } from '@/entity/expenses/list';
|
||||
|
||||
type UseAddExpensesListButtonArgs = {
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
@ -21,16 +23,9 @@ export const useAddExpensesListButton = (args: UseAddExpensesListButtonArgs) =>
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const title = formData.get('title');
|
||||
|
||||
if (typeof title === 'string') {
|
||||
await addExpensesListMutation({ title });
|
||||
setIsOpen(false);
|
||||
}
|
||||
const onFormSubmitHandler = async (data: AddExpenseListFormValues) => {
|
||||
await addExpensesListMutation(data);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import AddExpensesListButton from './AddExpensesListButton.ui';
|
||||
|
||||
import type { AddExpenseListBody, AddExpenseListSuccessResponse } from '@/entity/expenses/list';
|
||||
|
||||
const renderButton = () => {
|
||||
render(
|
||||
<AddExpensesListButton data-testid="test-button" />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId('test-button');
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const fillForm = async (data: { title: string }) => {
|
||||
await userEvent.type(screen.getByLabelText('Название'), data.title);
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.post<never, AddExpenseListBody, AddExpenseListSuccessResponse>(
|
||||
'http://localhost:3000/expenses/list',
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json({
|
||||
id: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
title: 'Mock list',
|
||||
items: [],
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('Test AddExpensesListButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Добавить');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
let submitButtonElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(submitHandler(requestSpy));
|
||||
modalElem = (await openModal()).modalElem;
|
||||
submitButtonElem = within(modalElem).getByText('Добавить');
|
||||
|
||||
await fillForm({
|
||||
title: 'Название списка',
|
||||
});
|
||||
});
|
||||
|
||||
test('should send correct request', async () => {
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
title: 'Название списка',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
import { AddExpensesListForm } from '@/entity/expenses/list';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useAddExpensesListButton } from '../hooks/useAddExpensesListButton';
|
||||
@ -27,13 +27,7 @@ const AddExpensesListButton: FC<AddExpensesListButtonProps> = (props) => {
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<form onSubmit={onFormSubmitHandler}>
|
||||
<h2>Добавить список расходов</h2>
|
||||
|
||||
<Input disabled={isPending} label="Название" name="title" />
|
||||
|
||||
<button disabled={isPending} type="submit">Добавить</button>
|
||||
</form>
|
||||
<AddExpensesListForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { type FormEventHandler, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type ExpenseList, useDeleteExpensesList } from '@/entity/expenses/list';
|
||||
|
||||
@ -20,8 +20,7 @@ export const useDeleteExpensesListButton = (args: UseDeleteExpensesListButtonArg
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
const onFormSubmitHandler = async () => {
|
||||
await deleteExpensesListAsync({ id });
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import DeleteExpensesListButton from './DeleteExpensesListButton.ui';
|
||||
|
||||
import type { DeleteExpenseListPath } from '@/entity/expenses/list';
|
||||
|
||||
const deletingList = {
|
||||
id: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
title: 'Mock list',
|
||||
items: [],
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
render(
|
||||
<DeleteExpensesListButton data-testid="delete-expense-list-button" list={deletingList} />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId('delete-expense-list-button');
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = http.delete<DeleteExpenseListPath>(`http://localhost:3000/expenses/list/${deletingList.id}`, () => HttpResponse.json(deletingList));
|
||||
|
||||
describe('Test DeleteExpensesListButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Удалить');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
server.use(submitHandler);
|
||||
const { modalElem } = await openModal();
|
||||
const submitButtonElem = within(modalElem).getByText('Удалить');
|
||||
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -1,3 +1,4 @@
|
||||
import { DeleteExpensesListForm } from '@/entity/expenses/list';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useDeleteExpensesListButton } from '../hooks/useDeleteExpensesListButton';
|
||||
@ -12,7 +13,7 @@ type DeleteExpensesListButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
|
||||
const DeleteExpensesListButton = (props: DeleteExpensesListButtonProps) => {
|
||||
const { list, ...restProps } = props;
|
||||
const { id, title } = list;
|
||||
const { id } = list;
|
||||
const {
|
||||
isOpen,
|
||||
isPending,
|
||||
@ -28,21 +29,11 @@ const DeleteExpensesListButton = (props: DeleteExpensesListButtonProps) => {
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<form onSubmit={onFormSubmitHandler}>
|
||||
<h2>Удалить список расходов</h2>
|
||||
|
||||
<p>
|
||||
Вы действительно хотите удалить список расходов
|
||||
{' '}
|
||||
|
||||
{`"${title}"`}
|
||||
|
||||
{' '}
|
||||
?
|
||||
</p>
|
||||
|
||||
<button disabled={isPending} type="submit">Удалить</button>
|
||||
</form>
|
||||
<DeleteExpensesListForm
|
||||
disabled={isPending}
|
||||
list={list}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { type FormEventHandler, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type ExpenseList, useEditExpensesList } from '@/entity/expenses/list';
|
||||
import { useEditExpensesList } from '@/entity/expenses/list';
|
||||
|
||||
import type { EditExpenseListFormValues, ExpenseList } from '@/entity/expenses/list';
|
||||
|
||||
type UseEditExpensesListButtonArgs = {
|
||||
id: ExpenseList['id'];
|
||||
@ -20,18 +22,12 @@ export const useEditExpensesListButton = (args: UseEditExpensesListButtonArgs) =
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const title = formData.get('title');
|
||||
|
||||
if (typeof title === 'string') {
|
||||
await editExpensesListAsync({
|
||||
path: { id },
|
||||
data: { title },
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
const onFormSubmitHandler = async (data: EditExpenseListFormValues) => {
|
||||
await editExpensesListAsync({
|
||||
path: { id },
|
||||
data,
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import EditExpensesListButton from './EditExpensesListButton.ui';
|
||||
|
||||
import type {
|
||||
EditExpenseListBody,
|
||||
EditExpenseListPath,
|
||||
EditExpenseListSuccessResponse,
|
||||
} from '@/entity/expenses/list';
|
||||
|
||||
const editingList = {
|
||||
id: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
title: 'Mock list',
|
||||
items: [],
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
render(
|
||||
<EditExpensesListButton data-testid="edit-expense-list-button" list={editingList} />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId('edit-expense-list-button');
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.patch<
|
||||
EditExpenseListPath,
|
||||
EditExpenseListBody,
|
||||
EditExpenseListSuccessResponse
|
||||
>(
|
||||
`http://localhost:3000/expenses/list/${editingList.id}`,
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json(editingList);
|
||||
},
|
||||
);
|
||||
|
||||
const fillForm = async (data: { title: string }) => {
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
await userEvent.clear(titleInputElem);
|
||||
await userEvent.type(titleInputElem, data.title);
|
||||
};
|
||||
|
||||
describe('Test EditExpensesListButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Редактировать');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
let submitButtonElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(submitHandler(requestSpy));
|
||||
modalElem = (await openModal()).modalElem;
|
||||
submitButtonElem = screen.getByText('Сохранить');
|
||||
|
||||
await fillForm({
|
||||
title: 'Название списка',
|
||||
});
|
||||
});
|
||||
|
||||
test('should send correct request', async () => {
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
title: 'Название списка',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
import { EditExpensesListForm } from '@/entity/expenses/list';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useEditExpensesListButton } from '../hooks/useEditExpensesListButton';
|
||||
@ -29,18 +29,7 @@ const EditExpensesListButton: FC<EditExpensesListButtonProps> = (props) => {
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<form onSubmit={onFormSubmitHandler}>
|
||||
<h2>Редактировать список расходов</h2>
|
||||
|
||||
<Input
|
||||
defaultValue={list.title}
|
||||
disabled={isPending}
|
||||
label="Название"
|
||||
name="title"
|
||||
/>
|
||||
|
||||
<button disabled={isPending} type="submit">Сохранить</button>
|
||||
</form>
|
||||
<EditExpensesListForm disabled={isPending} list={list} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -16,7 +16,7 @@ const Modal: FC<ModalProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper} onClick={onClose}>
|
||||
<div className={classes.wrapper} role="dialog" onClick={onClose}>
|
||||
<div className={classes.content} onClick={(e) => { e.stopPropagation(); }}>
|
||||
<span className={classes.closeIcon} onClick={onClose}>×</span>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user