diff --git a/src/App.test.jsx b/src/App.test.jsx index 22a4a58..dcc42d6 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -24,6 +24,9 @@ describe('App', () => { useSelector.mockImplementation((selector) => selector({ groups: STUDY_GROUPS, group: STUDY_GROUP, + writeField: { + tags: [], + }, })); }); diff --git a/src/components/write/TagsForm.jsx b/src/components/write/TagsForm.jsx new file mode 100644 index 0000000..1e72887 --- /dev/null +++ b/src/components/write/TagsForm.jsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; + +import styled from '@emotion/styled'; + +const TagsFormWrapper = styled.div``; + +const TagsForm = ({ onChange, tags }) => { + const [tag, setTag] = useState(''); + const [inputTags, setInputTags] = useState([]); + + const validateInput = (value) => { + if (!value) { + return; + } + + if (inputTags.includes(value)) { + return; + } + + const resultTags = [...inputTags, value]; + + setInputTags(resultTags); + onChange(resultTags); + }; + + const handleChange = (e) => { + setTag(e.target.value); + }; + + const handleRemove = (removeTag) => { + const removeTags = inputTags.filter((value) => value !== removeTag); + + setInputTags(removeTags); + onChange(removeTags); + }; + + const handleSubmit = () => { + validateInput(tag.trim()); + setTag(''); + }; + + useEffect(() => { + setInputTags(tags); + }, [tags]); + + return ( + +

태그

+ +
+ {inputTags.map((inputTag) => ( + handleRemove(inputTag)} + onKeyPress={() => {}} + role="button" + tabIndex="-1" + > + {`#${inputTag}`} + + ))} +
+
+ ); +}; + +export default TagsForm; diff --git a/src/components/write/TagsForm.test.jsx b/src/components/write/TagsForm.test.jsx new file mode 100644 index 0000000..f052342 --- /dev/null +++ b/src/components/write/TagsForm.test.jsx @@ -0,0 +1,117 @@ +import React from 'react'; + +import { fireEvent, render } from '@testing-library/react'; + +import TagsForm from './TagsForm'; + +describe('TagsForm', () => { + const handleChange = jest.fn(); + + beforeEach(() => { + handleChange.mockClear(); + }); + + const renderTagsTagsForm = (tags) => render(( + + )); + + describe('render Tag Form Container contents text', () => { + it('renders tag form text', () => { + const { getByPlaceholderText, container } = renderTagsTagsForm([]); + + expect(getByPlaceholderText('태그를 입력하세요')).not.toBeNull(); + expect(container).toHaveTextContent('태그'); + }); + }); + + describe('Check input tag validate', () => { + context('with tag input value', () => { + const tags = ['JavaScript', 'React']; + it('listens write field change events', () => { + const { getByPlaceholderText } = renderTagsTagsForm(['CSS']); + + const input = getByPlaceholderText('태그를 입력하세요'); + + tags.forEach((tag) => { + fireEvent.change(input, { target: { value: tag } }); + + fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 }); + + expect(input).toHaveValue(''); + }); + expect(handleChange).toBeCalledTimes(2); + }); + }); + + context('without tag input value', () => { + const tags = []; + it("doesn't listens write field change events", () => { + const { getByPlaceholderText } = renderTagsTagsForm(tags); + + const input = getByPlaceholderText('태그를 입력하세요'); + + fireEvent.change(input, { target: { value: '' } }); + + fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 }); + + expect(handleChange).not.toBeCalled(); + }); + }); + + describe('It is not executed because the current tag value is included.', () => { + const tags = ['JavaScript', 'React']; + it('listens write field change events', () => { + const { getByPlaceholderText } = renderTagsTagsForm(tags); + + const input = getByPlaceholderText('태그를 입력하세요'); + + tags.forEach((tag) => { + fireEvent.change(input, { target: { value: tag } }); + + fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 }); + + expect(input).toHaveValue(''); + + expect(handleChange).not.toBeCalled(); + }); + }); + }); + }); + + context('with tags', () => { + const tags = ['JavaScript', 'React']; + + it('renders Tags are below the input window', () => { + const { container } = renderTagsTagsForm(tags); + + expect(container).toHaveTextContent('#JavaScript'); + expect(container).toHaveTextContent('#React'); + }); + + it('Click event remove tag', () => { + const { getByText, container } = renderTagsTagsForm(tags); + + tags.forEach((tag) => { + fireEvent.click(getByText(`#${tag}`)); + + expect(container).not.toHaveTextContent(`#${tag}`); + }); + + expect(handleChange).toBeCalledTimes(2); + }); + }); + + context('without tags', () => { + const tags = []; + + it('renders Tags are below the input window', () => { + const { container } = renderTagsTagsForm(tags); + + expect(container).not.toHaveTextContent('#JavaScript'); + expect(container).not.toHaveTextContent('#React'); + }); + }); +}); diff --git a/src/containers/write/TagsFormContainer.jsx b/src/containers/write/TagsFormContainer.jsx new file mode 100644 index 0000000..aa7db8b --- /dev/null +++ b/src/containers/write/TagsFormContainer.jsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { useDispatch, useSelector } from 'react-redux'; + +import { get } from '../../util/utils'; +import TagsForm from '../../components/write/TagsForm'; +import { changeWriteField } from '../../reducers/slice'; + +const TagsFormContainer = () => { + const dispatch = useDispatch(); + + const { tags } = useSelector(get('writeField')); + + const onChangeTags = (nextTags) => { + dispatch( + changeWriteField({ + name: 'tags', + value: nextTags, + }), + ); + }; + + return ( + + ); +}; + +export default TagsFormContainer; diff --git a/src/containers/write/TagsFormContainer.test.jsx b/src/containers/write/TagsFormContainer.test.jsx new file mode 100644 index 0000000..ead8238 --- /dev/null +++ b/src/containers/write/TagsFormContainer.test.jsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { fireEvent, render } from '@testing-library/react'; + +import { useDispatch, useSelector } from 'react-redux'; + +import TagsFormContainer from './TagsFormContainer'; + +describe('TagsFormContainer', () => { + const dispatch = jest.fn(); + + beforeEach(() => { + dispatch.mockClear(); + + useDispatch.mockImplementation(() => dispatch); + + useSelector.mockImplementation((state) => state({ + writeField: { + tags: [], + }, + })); + }); + + const renderTagsFormContainer = () => render(( + + )); + + describe('render Tag Form Container contents text', () => { + it('renders tag form text', () => { + const { getByPlaceholderText, container } = renderTagsFormContainer(); + + expect(getByPlaceholderText('태그를 입력하세요')).not.toBeNull(); + expect(container).toHaveTextContent('태그'); + }); + }); + + describe('calls dispatch tags change action', () => { + const tags = ['JavaScript', 'React']; + it('change tags', () => { + const { getByPlaceholderText } = renderTagsFormContainer(); + + const input = getByPlaceholderText('태그를 입력하세요'); + + tags.forEach((tag) => { + fireEvent.change(input, { target: { value: tag } }); + + fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 }); + + expect(input).toHaveValue(''); + }); + expect(dispatch).toBeCalledWith({ + type: 'application/changeWriteField', + payload: { name: 'tags', value: tags }, + }); + }); + }); +}); diff --git a/src/pages/WritePage.jsx b/src/pages/WritePage.jsx index 363e710..64f616b 100644 --- a/src/pages/WritePage.jsx +++ b/src/pages/WritePage.jsx @@ -1,5 +1,7 @@ import React from 'react'; +import TagFormContainer from '../containers/write/TagsFormContainer'; + import Responsive from '../styles/Responsive'; const IntroducePage = () => ( @@ -9,11 +11,17 @@ const IntroducePage = () => (
-