Skip to content

Commit 9713917

Browse files
committed
feat: Add tests
1 parent 838e1dd commit 9713917

File tree

10 files changed

+159
-4
lines changed

10 files changed

+159
-4
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
4+
import { ContactForm } from '../components/contact-form';
5+
6+
jest.mock('@/shared/services/email-service', () => ({
7+
sendEmail: jest.fn(async () => await Promise.resolve(true))
8+
}));
9+
10+
jest.mock('@/shared/helpers/notify', () => ({
11+
notify: {
12+
success: jest.fn(),
13+
error: jest.fn()
14+
}
15+
}));
16+
17+
describe('ContactForm', () => {
18+
it('should validate and submit', async () => {
19+
render(<ContactForm />);
20+
const user = userEvent.setup();
21+
22+
await user.type(screen.getByTestId('email-input'), 'ramin@ramin.com');
23+
await user.type(screen.getByTestId('subject-input'), 'this is a subject');
24+
await user.type(
25+
screen.getByTestId('message-input'),
26+
'This is a long enough message to pass validation rules.'
27+
);
28+
29+
const submitButton = await screen.findByTestId('submit-button');
30+
await user.click(submitButton);
31+
32+
expect(true).toBe(true);
33+
});
34+
35+
it('should show validation errors', async () => {
36+
render(<ContactForm />);
37+
const user = userEvent.setup();
38+
39+
await user.type(screen.getByTestId('subject-input'), 'short');
40+
await user.type(screen.getByTestId('email-input'), 'ramin@ramin');
41+
await user.type(screen.getByTestId('message-input'), 'short message');
42+
43+
const submitButton = await screen.findByTestId('submit-button');
44+
await user.click(submitButton);
45+
46+
expect(screen.getByText(/Your email address is invalid!/i)).toBeInTheDocument();
47+
expect(
48+
screen.getByText(/Your subject should be more than 10 characters/i)
49+
).toBeInTheDocument();
50+
expect(
51+
screen.getByText(/Your message should be more than 30 characters/i)
52+
).toBeInTheDocument();
53+
});
54+
});

src/domains/contact-me/components/contact-form/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function ContactForm() {
7979
type="text"
8080
id="subject"
8181
label="Subject"
82+
testId="subject-input"
8283
placeholder="Enter your subject"
8384
error={errors.subject?.message}
8485
{...register('subject', {
@@ -97,6 +98,7 @@ export function ContactForm() {
9798
id="email"
9899
type="email"
99100
label="Email"
101+
testId="email-input"
100102
error={errors.email?.message}
101103
placeholder="Enter your email address"
102104
{...register('email', {
@@ -132,13 +134,14 @@ export function ContactForm() {
132134
type="textarea"
133135
label="Message"
134136
onChange={onChange}
137+
testId="message-input"
135138
placeholder="Enter your message"
136139
error={errors.message?.message}
137140
/>
138141
)}
139142
/>
140143
<div className="mt-2 flex w-full justify-end">
141-
<Button label="Submit" type="submit" loading={loading} />
144+
<Button testId="submit-button" label="Submit" type="submit" loading={loading} />
142145
</div>
143146
</form>
144147
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { render, screen } from '@testing-library/react';
2+
3+
import { PostTags } from '../components/post-tags';
4+
5+
describe('PostTags', () => {
6+
it('should render tags as links', () => {
7+
render(<PostTags postId={1} tags={['hooks', 'design']} />);
8+
const linkElement = screen.getByText('hooks');
9+
10+
expect(linkElement.closest('a')).toBeTruthy();
11+
expect(linkElement.closest('a')).toHaveAttribute('href', '/posts?tag=hooks');
12+
});
13+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { render, screen } from '@testing-library/react';
2+
3+
import type { PostFilters } from '@/shared/types/post';
4+
5+
import { PostsCategoryFilter } from '../components/posts-category-filter';
6+
7+
jest.mock('next/navigation', () => ({
8+
useRouter() {
9+
return {
10+
route: '/',
11+
query: '',
12+
asPath: '',
13+
pathname: '',
14+
events: {
15+
on: jest.fn(),
16+
off: jest.fn()
17+
},
18+
push: jest.fn(),
19+
prefetch: jest.fn(() => null),
20+
beforePopState: jest.fn(() => null)
21+
};
22+
}
23+
}));
24+
25+
describe('PostsCategoryFilter', () => {
26+
it('should render clear filter link with correct href', () => {
27+
const activeFilters: PostFilters = {
28+
tag: '',
29+
category: 'react'
30+
};
31+
32+
render(
33+
<PostsCategoryFilter
34+
activeFilters={activeFilters}
35+
categories={['react', 'system']}
36+
/>
37+
);
38+
39+
const link = screen.getByTestId('clear-filter-link');
40+
41+
expect(link).toBeInTheDocument();
42+
expect(link).toHaveAttribute('href', '/posts');
43+
});
44+
});

src/domains/posts/components/posts-category-filter/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function PostsCategoryFilter({
4747
{!!activeFilters && (
4848
<Link
4949
href={ROUTES.POSTS}
50+
data-testid="clear-filter-link"
5051
onClick={() => sendGTMEvent(GTM_EVENTS.CLEAR_FILTERS)}
5152
className="px-2 py-1 text-md bg-transparent backdrop-blur-sm hover:text-amber-500 duration-300"
5253
>

src/shared/components/button/button.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,10 @@ describe('<Button />', () => {
8080
await user.keyboard(' ');
8181
expect(onClick).toHaveBeenCalledTimes(2);
8282
});
83+
84+
test('testId is set correctly', () => {
85+
render(<Button label="Test" type="button" testId="test-button" />);
86+
const btn = screen.getByTestId('test-button');
87+
expect(btn).toBeInTheDocument();
88+
});
8389
});

src/shared/components/button/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Spinner } from '@/shared/components/spinner';
55

66
interface ButtonProps {
77
label: string;
8+
testId?: string;
89
loading?: boolean;
910
className?: string;
1011
disabled?: boolean;
@@ -15,6 +16,7 @@ interface ButtonProps {
1516
export function Button({
1617
type = 'button',
1718
label,
19+
testId,
1820
onClick,
1921
className,
2022
loading = false,
@@ -26,6 +28,7 @@ export function Button({
2628
title={label}
2729
onClick={onClick}
2830
aria-label={label}
31+
data-testid={testId}
2932
disabled={loading || disabled}
3033
className={clsx(
3134
'flex min-w-36 items-center justify-center gap-2 border px-5 leading-10 shadow backdrop-blur-md duration-300 hover:border-amber-500 hover:shadow-amber-500/50',

src/shared/components/text-input/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface TextInputProps {
99
name?: string;
1010
value?: string;
1111
label?: string;
12+
testId?: string;
1213
tabIndex?: number;
1314
required?: boolean;
1415
className?: string;
@@ -25,6 +26,7 @@ export function TextInput({
2526
label,
2627
value,
2728
error,
29+
testId,
2830
onChange,
2931
tabIndex,
3032
className,
@@ -58,6 +60,7 @@ export function TextInput({
5860
onChange={onChange}
5961
autoFocus={autoFocus}
6062
placeholder={placeholder}
63+
data-testid={testId ?? id}
6164
className={clsx(INPUT_CLASSES, {
6265
'border-red-500': !!error
6366
})}
@@ -73,6 +76,7 @@ export function TextInput({
7376
required={required}
7477
onChange={onChange}
7578
placeholder={placeholder}
79+
data-testid={testId ?? id}
7680
className={clsx(INPUT_CLASSES, 'min-h-48 px-4 py-2', {
7781
'border-red-500': !!error
7882
})}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
3+
import { useDebounce } from './use-debounce';
4+
5+
jest.useFakeTimers();
6+
7+
describe('useDebounce', () => {
8+
it('should debounce value', () => {
9+
const { result, rerender } = renderHook(
10+
({ value, delay }) => useDebounce(value, delay),
11+
{
12+
initialProps: { value: 'a', delay: 300 }
13+
}
14+
);
15+
16+
expect(result.current).toBe('a');
17+
18+
rerender({ value: 'ab', delay: 300 });
19+
expect(result.current).toBe('a');
20+
21+
act(() => {
22+
jest.advanceTimersByTime(300);
23+
});
24+
25+
expect(result.current).toBe('ab');
26+
});
27+
});

src/shared/services/search-service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { ENDPOINTS } from '@/shared/api/constants';
22
import { PostMetadata } from '@/shared/types/post';
33
import { notify } from '@/shared/helpers';
44

5-
interface Response {
5+
export type SearchPostsResponse = {
66
success: boolean;
77
message?: string;
88
data?: PostMetadata[];
9-
}
9+
};
1010

1111
export const searchPosts = (value: string) => {
1212
return new Promise((resolve, reject) => {
@@ -18,7 +18,7 @@ export const searchPosts = (value: string) => {
1818
}
1919
})
2020
.then((rawResponse) => rawResponse.json())
21-
.then((response: Response) => {
21+
.then((response: SearchPostsResponse) => {
2222
if (response.success) {
2323
resolve(response?.data ?? []);
2424
} else {

0 commit comments

Comments
 (0)