Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refacotor: faq 테스트 코드 작성 #340

Merged
merged 4 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib",
Expand Down
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"analyze": "ANALYZE=true next build",
"prepare": "husky install",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "storybook build",
"test": "vitest"
},
"dependencies": {
"@emotion/react": "^11.10.6",
Expand All @@ -45,11 +46,15 @@
"@storybook/react": "^7.4.0",
"@storybook/testing-library": "^0.2.0",
"@swc-jotai/react-refresh": "^0.0.4",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"@types/node": "18.14.2",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vitejs/plugin-react": "^4.2.1",
"chromatic": "^7.1.0",
"compression-webpack-plugin": "^10.0.0",
"eslint": "8.35.0",
Expand All @@ -60,12 +65,15 @@
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-storybook": "^0.6.13",
"eslint-plugin-unused-imports": "^2.0.0",
"happy-dom": "^12.10.3",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"next-sitemap": "^3.1.54",
"prettier": "^2.8.4",
"storybook": "^7.4.0",
"storybook-addon-next": "^1.8.0",
"typescript": "4.9.5"
"typescript": "4.9.5",
"vite-tsconfig-paths": "^4.2.3",
"vitest": "^1.1.3"
}
}
191 changes: 191 additions & 0 deletions src/components/FAQ/FAQ.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';

import { FAQ } from '~/components/FAQ/FAQ';

// TODO: @kimyouknow tsconfig 파일 정리해서 경로 alias 적용하기
import { Provider } from '../../../tests/setup';

describe('FAQ 컴포넌트 레이아웃 테스트', () => {
it('🟢 메인 타이틀에 "자주 묻는 질문"이 표시된다.', () => {
render(<FAQ />, {
wrapper: Provider,
});

expect(screen.getByRole('heading', { name: '자주 묻는 질문' })).toBeInTheDocument();
});
it('🟢 서브 타이틀에 "FAQ"가 표시된다.', () => {
render(<FAQ />, {
wrapper: Provider,
});

expect(screen.getByRole('heading', { name: 'FAQ' })).toBeInTheDocument();
});
it('🟢 [지원자격]탭만 활성화된 상태로 렌더링한다. ', () => {
render(<FAQ />, {
wrapper: Provider,
});

expect(screen.getByRole('tab', { name: '지원자격', selected: true })).toBeInTheDocument();
});
it('🔴 [지원자격] 탭 외에 다른 탭은 활성화되지 않은 상태로 렌더링한다.', () => {
render(<FAQ />, {
wrapper: Provider,
});

expect(screen.getByRole('tab', { name: '면접', selected: false })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: '활동', selected: false })).toBeInTheDocument();
});

it('🟢 가장 위에 있는 질문은 답변이 보이는 상태로 렌더링한다.', () => {
render(<FAQ />, {
wrapper: Provider,
});

const questionListContainer = screen.getByRole('list', { name: /faq-list/ });
const questionItems = within(questionListContainer).getAllByRole('button');

expect(questionItems[0]).toHaveAttribute('aria-expanded', 'true');
});
it('🟢 가장 위에 있는 질문 외에 나머지 질문들은 답변이 보이지 않은 상태로 렌더링한다.', () => {
render(<FAQ />, {
wrapper: Provider,
});

const questionListContainer = screen.getByRole('list', { name: /faq-list/ });
const [, ...restQuestionItems] = within(questionListContainer).getAllByRole('button');

restQuestionItems.forEach(x => {
expect(x).toHaveAttribute('aria-expanded', 'false');
});
});
});

describe('FAQ 컴포넌트 인터렉션 테스트', () => {
it('🟢 답변이 보이는 질문을 클릭하면 질문이 가려진다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

const questionListContainer = screen.getByRole('list', { name: /faq-list/ });
const questionItems = within(questionListContainer).getAllByRole('button');

expect(questionItems[0]).toHaveAttribute('aria-expanded', 'true');

await user.click(questionItems[0]);

expect(questionItems[0]).toHaveAttribute('aria-expanded', 'false');
});
it('🟢 답변이 가려진 질문을 클릭하면 질문이 보여진다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

const questionListContainer = screen.getByRole('list', { name: /faq-list/ });
const questionItems = within(questionListContainer).getAllByRole('button');

expect(questionItems[1]).toHaveAttribute('aria-expanded', 'false');

await user.click(questionItems[1]);

expect(questionItems[1]).toHaveAttribute('aria-expanded', 'true');
});
it('🟢 답변은 최대 1개만 보여준다. 유저 클릭으로 인해 답변이 1개 이상 보일 수 없다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

const questionListContainer = screen.getByRole('list', { name: /faq-list/ });
const questionItems = within(questionListContainer).getAllByRole('button');

expect(questionItems[0]).toHaveAttribute('aria-expanded', 'true');

await user.click(questionItems[1]);

expect(questionItems[0]).toHaveAttribute('aria-expanded', 'false');
expect(questionItems[1]).toHaveAttribute('aria-expanded', 'true');
questionItems.slice(2).forEach(x => {
expect(x).toHaveAttribute('aria-expanded', 'false');
});
});
it('🟢 [지원자격]탭을 클릭하면 [지원자격]질문들 항목을 표시한다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

await user.click(screen.getByRole('tab', { name: /지원자격/ }));

expect(screen.getByRole('tab', { name: '지원자격', selected: true })).toBeInTheDocument();

expect(screen.getByRole('list', { name: /지원자격/ })).toBeInTheDocument();
});
it('🟢 [면접]탭을 클릭하면 [면접]질문들 항목을 표시한다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

await user.click(screen.getByRole('tab', { name: '면접' }));

expect(screen.getByRole('tab', { name: '면접', selected: true })).toBeInTheDocument();

expect(screen.getByRole('list', { name: /면접/ })).toBeInTheDocument();
});
it('🟢 [활동]탭을 클릭하면 [활동]질문들 항목을 표시한다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

await user.click(screen.getByRole('tab', { name: '활동' }));

expect(screen.getByRole('tab', { name: '활동', selected: true })).toBeInTheDocument();

expect(screen.getByRole('list', { name: /활동/ })).toBeInTheDocument();
});
it('🟢 다른 탭을 클릭하면, 가장 위에 있는 질문은 답변이 보이는 상태로 렌더링한다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

await user.click(screen.getByRole('tab', { name: '면접' }));

const questionListContainer = screen.getByRole('list', { name: /면접/ });
const [fistQuestionItem] = within(questionListContainer).getAllByRole('button');

expect(fistQuestionItem).toHaveAttribute('aria-expanded', 'true');
});
it('🟢 다른 탭을 클릭하면, 가장 위에 있는 질문 외에 나머지 질문들은 답변이 보이지 않은 상태로 렌더링한다.', async () => {
const user = userEvent.setup();

render(<FAQ />, {
wrapper: Provider,
});

await user.click(screen.getByRole('tab', { name: '면접' }));

const questionListContainer = screen.getByRole('list', { name: /면접/ });
const [, ...restQuestionItems] = within(questionListContainer).getAllByRole('button');

restQuestionItems.forEach(x => {
expect(x).toHaveAttribute('aria-expanded', 'false');
});
});
});

describe('FAQ 컴포넌트 기타 기능 테스트', () => {
it.todo('🟢 활성화되지 않은 답변도 브라우저 검색 기능을 통해 검색되어야 한다.', () => {});
});
2 changes: 1 addition & 1 deletion src/components/FAQ/FAQ.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const Primary = {
};

export const List = {
render: () => <FAQList FAQList={FAQS} />,
render: () => <FAQList label="스토리북" FAQList={FAQS} />,
};

export const Interaction = {
Expand Down
20 changes: 11 additions & 9 deletions src/components/FAQ/FAQ.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ export function FAQ() {
<SectionTitle label="FAQ" title="자주 묻는 질문" />
<ul css={tabLayoutCss}>
{FAQ_GROUP.map(label => (
<li
key={label}
onClick={() => onClickTab(label)}
css={theme => tabCss(theme, isActive(label))}
>
{label}
<li key={label} onClick={() => onClickTab(label)}>
<button
role="tab"
aria-selected={isActive(label)}
css={theme => tabCss(theme, isActive(label))}
>
{label}
</button>
</li>
))}
</ul>
{activeTab === '지원자격' && <FAQList FAQList={지원자격질문들} />}
{activeTab === '면접' && <FAQList FAQList={면접질문들} />}
{activeTab === '활동' && <FAQList FAQList={활동질문들} />}
{activeTab === '지원자격' && <FAQList label="지원자격" FAQList={지원자격질문들} />}
{activeTab === '면접' && <FAQList label="면접" FAQList={면접질문들} />}
{activeTab === '활동' && <FAQList label="활동" FAQList={활동질문들} />}
</section>
);
}
Expand Down
13 changes: 8 additions & 5 deletions src/components/FAQ/FAQItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ interface FAQItemProps {

export function FAQItem({ isOpen, onClickOpenButton, question, answer }: FAQItemProps) {
return (
<li>
<li role="button" aria-expanded={isOpen} onClick={onClickOpenButton} css={liCss}>
<motion.div
css={theme => headerCss(theme, isOpen)}
animate={isOpen ? 'open' : 'closed'}
variants={headerVariants}
transition={{ duration: 0.3, ease: 'easeOut' }}
onClick={onClickOpenButton}
>
<h3>{question}</h3>
<h4>{question}</h4>
<motion.div variants={arrowIconVariants} transition={{ duration: 0.3, ease: 'easeOut' }}>
<ArrowIcon
direction={isOpen ? 'up' : 'down'}
Expand Down Expand Up @@ -58,14 +57,18 @@ const arrowIconVariants: Variants = {
closed: { stroke: theme.colors.blue400 },
};

const liCss = css`
cursor: pointer;
`;

const headerCss = (theme: Theme, isOpen: boolean) => css`
background-color: ${isOpen ? theme.colors.blue400 : theme.colors.black400};
display: flex;
justify-content: space-between;
align-items: center;
padding: 25px 30px;
cursor: pointer;
> h3 {

> h4 {
color: ${isOpen ? theme.colors.black800 : theme.colors.white};
text-align: center;
${theme.typos.pretendard.subTitle2}
Expand Down
19 changes: 10 additions & 9 deletions src/components/FAQ/FAQList.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { useEffect, useState } from 'react';
import { HTMLProps, useEffect, useState } from 'react';
import { css } from '@emotion/react';

import { FAQItem } from '~/components/FAQ/FAQItem';
import { FAQType } from '~/constant/faq';
import { mediaQuery } from '~/styles/media';

interface FAQListProps {
interface FAQListProps extends HTMLProps<HTMLUListElement> {
FAQList: FAQType[];
label: string;
}

const DEFAULT_OPEN = 0;
const CLOSE = -1;
const DEFAULT_OPEN_IDX = 0;
const CLOSE_IDX = -1;

export function FAQList({ FAQList }: FAQListProps) {
const [activeIndex, setActiveIndex] = useState(DEFAULT_OPEN);
export function FAQList({ FAQList, label, ...props }: FAQListProps) {
const [activeIndex, setActiveIndex] = useState(DEFAULT_OPEN_IDX);

const onClickActiveFaq = (idx: number) => {
/**
* 열려 있는 아이템 다시 클릭하면 닫히게 하기
*/
setActiveIndex(prev => (prev === idx ? CLOSE : idx));
setActiveIndex(prev => (prev === idx ? CLOSE_IDX : idx));
};

useEffect(() => {
setActiveIndex(DEFAULT_OPEN);
setActiveIndex(DEFAULT_OPEN_IDX);
}, []);

return (
<ul css={containerCss}>
<ul aria-label={`faq-list-${label}`} css={containerCss} {...props}>
{FAQList.map((item, index) => (
<FAQItem
key={item.question}
Expand Down
Loading
Loading