Skip to content

Commit

Permalink
feat: FAQ 컴포넌트 테스트 코드 작성 (#339)
Browse files Browse the repository at this point in the history
* refactor: 컴포넌트 aria-label 추가 및 시맨틱 태그 부여

* feat: faq 컴포넌트 테스트 코드 작성

* chore: 스토리북 props

* feat: [지원자격] 탭 활성 description 수정 및 테스트 세분화

* refactor: 답변이 보이는 질문 리스트 셀렉터 수정

* feat: 세부구현~
  • Loading branch information
kimyouknow authored Feb 12, 2024
1 parent cebfa8a commit 0a37947
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 48 deletions.
185 changes: 161 additions & 24 deletions src/components/FAQ/FAQ.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
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';
Expand All @@ -21,32 +22,168 @@ describe('FAQ 컴포넌트 레이아웃 테스트', () => {

expect(screen.getByRole('heading', { name: 'FAQ' })).toBeInTheDocument();
});
it.todo('🟢 [지원자격]탭이 활성화된 상태로 렌더링한다.', () => {});
it.todo('🟢 가장 위에 있는 질문은 답변이 보이는 상태로 렌더링한다.', () => {});
it.todo(
'🟢 가장 위에 있는 질문 외에 나머지 질문들은 답변이 보이지 않은 상태로 렌더링한다.',
() => {}
);
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.todo('🟢 답변이 보이는 질문을 클릭하면 질문이 가려진다.', () => {});
it.todo('🟢 답변이 가려진 질문을 클릭하면 질문이 보여진다.', () => {});
it.todo(
'🟢 답변은 최대 1개만 보여준다. 유저 클릭으로 인해 답변이 1개 이상 보일 수 없다.',
() => {}
);
it.todo('🟢 [지원자격]탭을 클릭하면 [지원자격]질문들 항목을 표시한다.', () => {});
it.todo('🟢 [면접]탭을 클릭하면 [면접]질문들 항목을 표시한다.', () => {});
it.todo('🟢 [활동]탭을 클릭하면 [활동]질문들 항목을 표시한다.', () => {});
it.todo(
'🟢 다른 탭을 클릭하면, 가장 위에 있는 질문은 답변이 보이는 상태로 렌더링한다.',
() => {}
);
it.todo(
'🟢 다른 탭을 클릭하면, 가장 위에 있는 질문 외에 나머지 질문들은 답변이 보이지 않은 상태로 렌더링한다.',
() => {}
);
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 컴포넌트 기타 기능 테스트', () => {
Expand Down
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

0 comments on commit 0a37947

Please sign in to comment.