Skip to content
4 changes: 0 additions & 4 deletions src/components/molecules/Button/index.module.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
.button {
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
height: 40px;
font-weight: bold;
Expand All @@ -16,7 +13,6 @@
border-color 0.3s ease-in-out,
opacity 0.3s ease-in-out;
white-space: nowrap;
flex-direction: row;
text-align: center;

&.menu {
Expand Down
36 changes: 36 additions & 0 deletions src/components/molecules/Button/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';

import Button from './index';

describe('Button', () => {
it('renders a native button with disabled state', () => {
render(
<Button buttonType="apply" disabled>
지원하기
</Button>,
);

const button = screen.getByRole('button', { name: '지원하기' });

expect(button).toBeDisabled();
expect(button).toHaveAttribute('aria-disabled', 'true');
});

it('renders an external link with safe link attributes', () => {
render(
<Button
buttonType="apply"
href="https://example.com/apply"
isExternalLink
>
지원하기
</Button>,
);

const link = screen.getByRole('link', { name: '지원하기' });

expect(link).toHaveAttribute('href', 'https://example.com/apply');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
});
33 changes: 19 additions & 14 deletions src/components/molecules/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ComponentProps, ReactNode } from 'react';
import { Route } from 'next';
import Link, { LinkProps } from 'next/link';

import { Flex } from '@sipe-team/side';
import clsx from 'clsx';

import styles from './index.module.scss';
Expand Down Expand Up @@ -62,24 +63,28 @@ function Button<
>;

return (
<Link
href={href as Route}
className={commonClassName}
aria-disabled={disabled}
rel={isExternalLink ? 'noopener noreferrer' : undefined}
target={isExternalLink ? '_blank' : undefined}
{...linkRest}
/>
<Flex asChild align="center" direction="row" justify="center">
<Link
href={href as Route}
className={commonClassName}
aria-disabled={disabled}
rel={isExternalLink ? 'noopener noreferrer' : undefined}
target={isExternalLink ? '_blank' : undefined}
{...linkRest}
/>
</Flex>
);
}

return (
<button
className={commonClassName}
disabled={disabled}
aria-disabled={disabled}
{...(rest as ComponentProps<'button'>)}
/>
<Flex asChild align="center" direction="row" justify="center">
<button
className={commonClassName}
disabled={disabled}
aria-disabled={disabled}
{...(rest as ComponentProps<'button'>)}
/>
</Flex>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { fireEvent, render, screen } from '@testing-library/react';

import SponsorImage from './SponsorImage';

vi.mock('next/image', () => ({
default: ({
alt,
onError,
src,
}: {
alt: string;
onError?: () => void;
src: string;
}) => (
// Tests only need an element with alt text semantics.
// eslint-disable-next-line @next/next/no-img-element
<img alt={alt} onError={onError} src={src} />
),
}));

describe('SponsorImage', () => {
it('renders the sponsor image with the provided alt text', () => {
render(<SponsorImage src="/sponsor.png" alt="후원사" />);

expect(screen.getByAltText('후원사')).toHaveAttribute(
'src',
'/sponsor.png',
);
});

it('renders the fallback image when the sponsor image fails to load', () => {
render(<SponsorImage src="/broken.png" alt="후원사" />);

fireEvent.error(screen.getByAltText('후원사'));

expect(screen.getByAltText('Image not available')).toHaveAttribute(
'src',
'/assets/empty_image.png',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from 'react';

import Image from 'next/image';

import { Flex } from '@sipe-team/side';
import clsx from 'clsx';

import styles from './index.module.scss';
Expand Down Expand Up @@ -37,9 +38,13 @@ function SponsorImage({ src, alt }: SponsorImageProps) {
};

return (
<div className={clsx(styles.imageWrapper, hasError && styles.errorWrapper)}>
<Flex
align="center"
className={clsx(styles.imageWrapper, hasError && styles.errorWrapper)}
justify="center"
>
<Image {...imageProperties} />
</div>
</Flex>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@
background-color: white;
border-radius: 12px;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;

&.errorWrapper {
background-color: #2d3748; // empty_image.png 배경색과 매칭
Expand Down
11 changes: 0 additions & 11 deletions src/components/organisms/activity/ActiveCard/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@
width: 100%;
margin-left: 24px;
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;

@include mobile {
margin-left: 0;
Expand Down Expand Up @@ -60,17 +57,9 @@
}

.posterUserInfo {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;

.userWrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;

.userIcon {
border-radius: 50%;
}
Expand Down
68 changes: 68 additions & 0 deletions src/components/organisms/activity/ActiveCard/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { render, screen } from '@testing-library/react';

import ActiveCard from './index';

vi.mock('@/components/molecules/Image', () => ({
default: ({ alt, src }: { alt: string; src?: string }) => (
// Tests only need an element with alt text semantics.
// eslint-disable-next-line @next/next/no-img-element
<img alt={alt} src={src} />
),
}));

vi.mock('@/libs/assets/icons', () => ({
UserIcon: ({ className }: { className?: string }) => (
<svg aria-label="default user profile" className={className} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 내용은 props와 무관하게 aria-label="default user profile"을 항상 렌더링해서,

아래에 있는,
expect(screen.getByLabelText('default user profile')).toBeInTheDocument();
에서 실제 UserIcon 의 접근성을 검증하지 못하고 있습니다

실제 코드에서 aria-label 값을 넘겨주도록 수정하시거나,
<UserIcon aria-label="default user profile" className={styles.userIcon} />

이 mock 을 패스 스루 해주시면 어떨까요?
UserIcon: (props: React.SVGProps<SVGSVGElement>) => <svg {...props} />,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다. mock이 aria-label을 직접 만들어 실제 컴포넌트의 접근성 계약을 검증하지 못하고 있었습니다. fallback UserIconaria-label을 명시하고, mock은 전달받은 props를 그대로 적용하도록 수정하겠습니다.

),
}));

describe('ActiveCard', () => {
it('renders the post thumbnail, content, profile, metadata, and outbound link', () => {
render(
<ActiveCard
thumbnail="/activity-post.png"
profile="/profile.png"
contentTitle="사이프 블로그 글"
contentBody="사이프 활동을 정리한 글입니다"
userName="김사이퍼"
createDate="2026.06.22"
link="https://example.com/post"
/>,
);

expect(screen.getByRole('link')).toHaveAttribute(
'href',
'https://example.com/post',
);
expect(screen.getByAltText('thumbnail')).toHaveAttribute(
'src',
'/activity-post.png',
);
expect(screen.getByAltText('user profile')).toHaveAttribute(
'src',
'/profile.png',
);
expect(screen.getByText('사이프 블로그 글')).toBeInTheDocument();
expect(
screen.getByText('사이프 활동을 정리한 글입니다'),
).toBeInTheDocument();
expect(screen.getByText('김사이퍼')).toBeInTheDocument();
expect(screen.getByText('2026.06.22')).toBeInTheDocument();
});

it('renders the default user icon when profile is omitted', () => {
render(
<ActiveCard
thumbnail="/activity-post.png"
contentTitle="프로필 없는 글"
contentBody="기본 아이콘 확인"
userName="박사이퍼"
createDate="2026.06.23"
link="https://example.com/no-profile"
/>,
);

expect(screen.getByLabelText('default user profile')).toBeInTheDocument();
expect(screen.queryByAltText('user profile')).not.toBeInTheDocument();
});
});
92 changes: 56 additions & 36 deletions src/components/organisms/activity/ActiveCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { color, Typography } from '@sipe-team/side';
import { color, Flex, Typography } from '@sipe-team/side';

import ExternalLink from '@/components/atoms/ExternalLink';
import Image from '@/components/molecules/Image';
Expand Down Expand Up @@ -40,42 +40,62 @@ function ActiveCard({
alt="thumbnail"
sizes="(max-width: 1060px) 50vw, 33vw"
/>
<article className={styles.contentsWrapper}>
<section className={styles.contents}>
<Typography
color={color.white}
className={styles.title}
size={20}
weight="bold"
>
{contentTitle}
</Typography>
<Typography className={styles.body} size={14}>
{contentBody}
</Typography>
</section>
<section className={styles.posterUserInfo}>
<div className={styles.userWrapper}>
{profile ? (
<Image
src={profile}
alt="user profile"
width={32}
height={32}
className={styles.userIcon}
/>
) : (
<UserIcon className={styles.userIcon} />
)}
<Typography color={color.white} size={14}>
{userName}
<Flex
asChild
className={styles.contentsWrapper}
direction="column"
justify="space-between"
>
<article>
<section className={styles.contents}>
<Typography
color={color.white}
className={styles.title}
size={20}
weight="bold"
>
{contentTitle}
</Typography>
<Typography className={styles.body} size={14}>
{contentBody}
</Typography>
</div>
<Typography className={styles.createDate} size={12} weight="semiBold">
{createDate}
</Typography>
</section>
</article>
</section>
<Flex
asChild
align="center"
className={styles.posterUserInfo}
justify="space-between"
>
<section>
<Flex asChild align="center" gap="12px" justify="center">
<div className={styles.userWrapper}>
{profile ? (
<Image
src={profile}
alt="user profile"
width={32}
height={32}
className={styles.userIcon}
/>
) : (
<UserIcon className={styles.userIcon} />
)}
<Typography color={color.white} size={14}>
{userName}
</Typography>
</div>
</Flex>
<Typography
className={styles.createDate}
size={12}
weight="semiBold"
>
{createDate}
</Typography>
</section>
</Flex>
</article>
</Flex>
</ExternalLink>
);
}
Expand Down
Loading
Loading