Skip to content

Commit 4082b24

Browse files
committed
test: add comprehensive test suite for client and server
1 parent 5a682e8 commit 4082b24

File tree

5 files changed

+654
-0
lines changed

5 files changed

+654
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { BrowserRouter } from 'react-router-dom';
4+
import Footer from '../../components/navigation/Footer';
5+
import appConfig from '../../config/appConfig';
6+
7+
// Mock appConfig to control test values
8+
vi.mock('../../config/appConfig', () => ({
9+
default: {
10+
version: '1.8.2',
11+
appName: 'PowerPulse',
12+
githubUrl: 'https://github.com/blink-zero/powerpulse',
13+
copyrightYear: 2025,
14+
copyrightOwner: 'blink-zero'
15+
}
16+
}));
17+
18+
describe('Footer Component', () => {
19+
// Helper function to render the Footer with Router context
20+
const renderFooter = () => {
21+
return render(
22+
<BrowserRouter>
23+
<Footer />
24+
</BrowserRouter>
25+
);
26+
};
27+
28+
it('renders without crashing', () => {
29+
renderFooter();
30+
expect(screen.getByRole('contentinfo')).toBeInTheDocument();
31+
});
32+
33+
it('displays the correct version number', () => {
34+
renderFooter();
35+
expect(screen.getByText(`v${appConfig.version}`)).toBeInTheDocument();
36+
});
37+
38+
it('displays the correct copyright information', () => {
39+
renderFooter();
40+
const year = appConfig.copyrightYear;
41+
const owner = appConfig.copyrightOwner;
42+
expect(screen.getByText(${year} ${owner}`)).toBeInTheDocument();
43+
});
44+
45+
it('includes a link to the GitHub repository', () => {
46+
renderFooter();
47+
const githubLink = screen.getByRole('link', { name: /github/i });
48+
expect(githubLink).toHaveAttribute('href', appConfig.githubUrl);
49+
expect(githubLink).toHaveAttribute('target', '_blank');
50+
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
51+
});
52+
53+
it('displays the app name', () => {
54+
renderFooter();
55+
expect(screen.getByText(appConfig.appName)).toBeInTheDocument();
56+
});
57+
58+
it('has the correct CSS classes for styling', () => {
59+
renderFooter();
60+
const footer = screen.getByRole('contentinfo');
61+
expect(footer).toHaveClass('bg-white');
62+
expect(footer).toHaveClass('dark:bg-gray-800');
63+
expect(footer).toHaveClass('border-t');
64+
});
65+
66+
it('is responsive with appropriate padding', () => {
67+
renderFooter();
68+
const footer = screen.getByRole('contentinfo');
69+
expect(footer).toHaveClass('px-4');
70+
expect(footer).toHaveClass('py-3');
71+
expect(footer).toHaveClass('sm:px-6');
72+
});
73+
});
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { renderHook, act } from '@testing-library/react';
3+
import { useFormValidation } from '../../hooks/useFormValidation';
4+
5+
describe('useFormValidation Hook', () => {
6+
const initialValues = {
7+
name: '',
8+
email: '',
9+
password: ''
10+
};
11+
12+
const validationRules = {
13+
name: {
14+
required: true,
15+
requiredMessage: 'Name is required',
16+
validator: (value) => value.length >= 3,
17+
message: 'Name must be at least 3 characters'
18+
},
19+
email: {
20+
required: true,
21+
requiredMessage: 'Email is required',
22+
validator: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
23+
message: 'Please enter a valid email address'
24+
},
25+
password: {
26+
required: true,
27+
requiredMessage: 'Password is required',
28+
validator: (value) => value.length >= 8,
29+
message: 'Password must be at least 8 characters'
30+
}
31+
};
32+
33+
const onSubmit = vi.fn();
34+
35+
it('should initialize with the provided values', () => {
36+
const { result } = renderHook(() =>
37+
useFormValidation(initialValues, validationRules, onSubmit)
38+
);
39+
40+
expect(result.current.values).toEqual(initialValues);
41+
expect(result.current.errors).toEqual({});
42+
expect(result.current.touched).toEqual({});
43+
expect(result.current.isSubmitting).toBe(false);
44+
});
45+
46+
it('should update values when handleChange is called', () => {
47+
const { result } = renderHook(() =>
48+
useFormValidation(initialValues, validationRules, onSubmit)
49+
);
50+
51+
act(() => {
52+
result.current.handleChange({
53+
target: { name: 'name', value: 'John Doe' }
54+
});
55+
});
56+
57+
expect(result.current.values.name).toBe('John Doe');
58+
});
59+
60+
it('should mark field as touched when handleBlur is called', () => {
61+
const { result } = renderHook(() =>
62+
useFormValidation(initialValues, validationRules, onSubmit)
63+
);
64+
65+
act(() => {
66+
result.current.handleBlur({
67+
target: { name: 'name' }
68+
});
69+
});
70+
71+
expect(result.current.touched.name).toBe(true);
72+
});
73+
74+
it('should validate required fields', () => {
75+
const { result } = renderHook(() =>
76+
useFormValidation(initialValues, validationRules, onSubmit)
77+
);
78+
79+
// Trigger validation by attempting to submit
80+
act(() => {
81+
result.current.handleSubmit({ preventDefault: vi.fn() });
82+
});
83+
84+
expect(result.current.errors.name).toBe('Name is required');
85+
expect(result.current.errors.email).toBe('Email is required');
86+
expect(result.current.errors.password).toBe('Password is required');
87+
});
88+
89+
it('should validate field format when values are provided', () => {
90+
const { result } = renderHook(() =>
91+
useFormValidation(
92+
{ ...initialValues, name: 'Jo', email: 'invalid-email', password: 'short' },
93+
validationRules,
94+
onSubmit
95+
)
96+
);
97+
98+
// Trigger validation by attempting to submit
99+
act(() => {
100+
result.current.handleSubmit({ preventDefault: vi.fn() });
101+
});
102+
103+
expect(result.current.errors.name).toBe('Name must be at least 3 characters');
104+
expect(result.current.errors.email).toBe('Please enter a valid email address');
105+
expect(result.current.errors.password).toBe('Password must be at least 8 characters');
106+
});
107+
108+
it('should call onSubmit when form is valid', async () => {
109+
const validValues = {
110+
name: 'John Doe',
111+
email: 'john@example.com',
112+
password: 'password123'
113+
};
114+
115+
const { result } = renderHook(() =>
116+
useFormValidation(validValues, validationRules, onSubmit)
117+
);
118+
119+
await act(async () => {
120+
await result.current.handleSubmit({ preventDefault: vi.fn() });
121+
});
122+
123+
expect(onSubmit).toHaveBeenCalledWith(validValues);
124+
});
125+
126+
it('should set isSubmitting during form submission', async () => {
127+
// Create a delayed onSubmit function
128+
const delayedOnSubmit = vi.fn().mockImplementation(() =>
129+
new Promise(resolve => setTimeout(resolve, 100))
130+
);
131+
132+
const validValues = {
133+
name: 'John Doe',
134+
email: 'john@example.com',
135+
password: 'password123'
136+
};
137+
138+
const { result } = renderHook(() =>
139+
useFormValidation(validValues, validationRules, delayedOnSubmit)
140+
);
141+
142+
let submissionPromise;
143+
144+
act(() => {
145+
submissionPromise = result.current.handleSubmit({ preventDefault: vi.fn() });
146+
});
147+
148+
// Check that isSubmitting is true during submission
149+
expect(result.current.isSubmitting).toBe(true);
150+
151+
// Wait for submission to complete
152+
await act(async () => {
153+
await submissionPromise;
154+
});
155+
156+
// Check that isSubmitting is false after submission
157+
expect(result.current.isSubmitting).toBe(false);
158+
});
159+
160+
it('should reset the form when resetForm is called', () => {
161+
const { result } = renderHook(() =>
162+
useFormValidation(
163+
{ ...initialValues, name: 'John Doe' },
164+
validationRules,
165+
onSubmit
166+
)
167+
);
168+
169+
// First, make some changes
170+
act(() => {
171+
result.current.handleChange({
172+
target: { name: 'email', value: 'john@example.com' }
173+
});
174+
175+
result.current.handleBlur({
176+
target: { name: 'name' }
177+
});
178+
});
179+
180+
// Then reset the form
181+
act(() => {
182+
result.current.resetForm();
183+
});
184+
185+
// Check that everything is reset
186+
expect(result.current.values).toEqual(initialValues);
187+
expect(result.current.errors).toEqual({});
188+
expect(result.current.touched).toEqual({});
189+
expect(result.current.isSubmitting).toBe(false);
190+
});
191+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { renderHook, act } from '@testing-library/react';
3+
import { useInactivityTimer } from '../../hooks/useInactivityTimer';
4+
5+
// Mock the window events
6+
const mockAddEventListener = vi.fn();
7+
const mockRemoveEventListener = vi.fn();
8+
9+
// Save original methods
10+
const originalAddEventListener = window.addEventListener;
11+
const originalRemoveEventListener = window.removeEventListener;
12+
13+
describe('useInactivityTimer Hook', () => {
14+
beforeEach(() => {
15+
// Mock the window methods
16+
window.addEventListener = mockAddEventListener;
17+
window.removeEventListener = mockRemoveEventListener;
18+
19+
// Reset mocks before each test
20+
vi.clearAllMocks();
21+
22+
// Mock Date.now to control time
23+
vi.useFakeTimers();
24+
});
25+
26+
afterEach(() => {
27+
// Restore original methods
28+
window.addEventListener = originalAddEventListener;
29+
window.removeEventListener = originalRemoveEventListener;
30+
31+
// Restore real timers
32+
vi.useRealTimers();
33+
});
34+
35+
it('should set up event listeners on mount', () => {
36+
const onTimeout = vi.fn();
37+
const timeoutMinutes = 30;
38+
39+
renderHook(() => useInactivityTimer(onTimeout, timeoutMinutes));
40+
41+
// Check that event listeners were added for all activity events
42+
expect(mockAddEventListener).toHaveBeenCalledWith('mousemove', expect.any(Function));
43+
expect(mockAddEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function));
44+
expect(mockAddEventListener).toHaveBeenCalledWith('keypress', expect.any(Function));
45+
expect(mockAddEventListener).toHaveBeenCalledWith('touchstart', expect.any(Function));
46+
expect(mockAddEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
47+
});
48+
49+
it('should clean up event listeners on unmount', () => {
50+
const onTimeout = vi.fn();
51+
const timeoutMinutes = 30;
52+
53+
const { unmount } = renderHook(() => useInactivityTimer(onTimeout, timeoutMinutes));
54+
55+
// Unmount the hook
56+
unmount();
57+
58+
// Check that event listeners were removed
59+
expect(mockRemoveEventListener).toHaveBeenCalledWith('mousemove', expect.any(Function));
60+
expect(mockRemoveEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function));
61+
expect(mockRemoveEventListener).toHaveBeenCalledWith('keypress', expect.any(Function));
62+
expect(mockRemoveEventListener).toHaveBeenCalledWith('touchstart', expect.any(Function));
63+
expect(mockRemoveEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
64+
});
65+
66+
it('should call onTimeout after the specified timeout period', () => {
67+
const onTimeout = vi.fn();
68+
const timeoutMinutes = 30;
69+
70+
renderHook(() => useInactivityTimer(onTimeout, timeoutMinutes));
71+
72+
// Fast-forward time past the timeout
73+
vi.advanceTimersByTime(timeoutMinutes * 60 * 1000 + 1000);
74+
75+
// Check that onTimeout was called
76+
expect(onTimeout).toHaveBeenCalled();
77+
});
78+
79+
it('should reset the timer when activity is detected', () => {
80+
const onTimeout = vi.fn();
81+
const timeoutMinutes = 30;
82+
83+
renderHook(() => useInactivityTimer(onTimeout, timeoutMinutes));
84+
85+
// Get the activity handler (first argument of the first call)
86+
const activityHandler = mockAddEventListener.mock.calls[0][1];
87+
88+
// Fast-forward time halfway through the timeout
89+
vi.advanceTimersByTime(timeoutMinutes * 30 * 1000);
90+
91+
// Simulate user activity
92+
act(() => {
93+
activityHandler();
94+
});
95+
96+
// Fast-forward time halfway through the timeout again
97+
vi.advanceTimersByTime(timeoutMinutes * 30 * 1000);
98+
99+
// onTimeout should not have been called yet
100+
expect(onTimeout).not.toHaveBeenCalled();
101+
102+
// Fast-forward time past the full timeout
103+
vi.advanceTimersByTime(timeoutMinutes * 30 * 1000 + 1000);
104+
105+
// Now onTimeout should have been called
106+
expect(onTimeout).toHaveBeenCalled();
107+
});
108+
});

0 commit comments

Comments
 (0)