Skip to content

Commit 8e4b113

Browse files
nilaychughkasya
andauthored
implemented AnimatedCounter tests (#1994)
* implemented Animated Counter tests * updated changes --------- Co-authored-by: Kate Golovanova <[email protected]>
1 parent f922963 commit 8e4b113

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import { render, screen, act } from '@testing-library/react'
2+
import '@testing-library/jest-dom'
3+
import AnimatedCounter from 'components/AnimatedCounter'
4+
5+
jest.useFakeTimers()
6+
7+
// Patch for performance.now() in jsdom test env
8+
beforeAll(() => {
9+
if (typeof performance.now !== 'function') {
10+
performance.now = jest.fn(() => Date.now())
11+
}
12+
})
13+
14+
describe('AnimatedCounter', () => {
15+
afterEach(() => {
16+
jest.clearAllTimers()
17+
})
18+
19+
describe('Renders successfully with minimal required props', () => {
20+
it('renders correctly with initial count 0', () => {
21+
render(<AnimatedCounter end={1000} duration={2} />)
22+
const counter = screen.getByText('0')
23+
expect(counter).toBeInTheDocument()
24+
})
25+
26+
it('renders with all props including className', () => {
27+
render(<AnimatedCounter end={50} duration={1} className="test-class" />)
28+
const element = screen.getByText('0')
29+
expect(element).toHaveClass('test-class')
30+
})
31+
})
32+
33+
describe('Prop-based behavior – different props affect output', () => {
34+
it('renders with correct end value', () => {
35+
render(<AnimatedCounter end={1000} duration={1} />)
36+
expect(screen.getByText('0')).toBeInTheDocument()
37+
})
38+
39+
it('applies custom className when provided', () => {
40+
render(<AnimatedCounter end={100} duration={2} className="custom-counter" />)
41+
const element = screen.getByText('0')
42+
expect(element).toHaveClass('custom-counter')
43+
})
44+
45+
it('renders without className when not provided', () => {
46+
render(<AnimatedCounter end={100} duration={2} />)
47+
const element = screen.getByText('0')
48+
expect(element).not.toHaveAttribute('class')
49+
})
50+
})
51+
52+
describe('State changes / internal logic', () => {
53+
it('animates to the end value over duration', () => {
54+
render(<AnimatedCounter end={1000} duration={2} />)
55+
56+
// Advance time by 2 seconds within act()
57+
act(() => {
58+
jest.advanceTimersByTime(2000)
59+
})
60+
61+
const counter = screen.getByText('1K')
62+
expect(counter).toBeInTheDocument()
63+
})
64+
65+
it('updates count during animation', () => {
66+
render(<AnimatedCounter end={50} duration={2} />)
67+
68+
// Advance time by 1 second (halfway through animation)
69+
act(() => {
70+
jest.advanceTimersByTime(1000)
71+
})
72+
73+
// Should show intermediate value
74+
const displayedValue = parseInt(screen.getByText(/\d+/).textContent || '0')
75+
expect(displayedValue).toBeGreaterThan(0)
76+
expect(displayedValue).toBeLessThanOrEqual(50)
77+
})
78+
79+
it('stops at exact end value', () => {
80+
render(<AnimatedCounter end={75} duration={1} />)
81+
82+
act(() => {
83+
jest.advanceTimersByTime(1000)
84+
})
85+
86+
expect(screen.getByText('75')).toBeInTheDocument()
87+
})
88+
})
89+
90+
describe('Default values and fallbacks', () => {
91+
it('handles zero end value', () => {
92+
render(<AnimatedCounter end={0} duration={1} />)
93+
expect(screen.getByText('0')).toBeInTheDocument()
94+
})
95+
96+
it('handles negative end value', () => {
97+
render(<AnimatedCounter end={-10} duration={1} />)
98+
expect(screen.getByText('0')).toBeInTheDocument()
99+
})
100+
101+
it('handles very small duration', () => {
102+
render(<AnimatedCounter end={100} duration={0.1} />)
103+
expect(screen.getByText('0')).toBeInTheDocument()
104+
})
105+
106+
it('handles very large duration', () => {
107+
render(<AnimatedCounter end={100} duration={100} />)
108+
expect(screen.getByText('0')).toBeInTheDocument()
109+
})
110+
})
111+
112+
describe('Text and content rendering', () => {
113+
it('displays formatted numbers using millify', () => {
114+
render(<AnimatedCounter end={1200} duration={1} />)
115+
116+
act(() => {
117+
jest.advanceTimersByTime(1000)
118+
})
119+
120+
expect(screen.getByText('1.2K')).toBeInTheDocument()
121+
})
122+
123+
it('renders as span element', () => {
124+
render(<AnimatedCounter end={100} duration={2} />)
125+
const element = screen.getByText('0')
126+
expect(element.tagName).toBe('SPAN')
127+
})
128+
})
129+
130+
describe('Handles edge cases and invalid inputs', () => {
131+
it('handles decimal end values', () => {
132+
render(<AnimatedCounter end={99.5} duration={1} />)
133+
expect(screen.getByText('0')).toBeInTheDocument()
134+
})
135+
136+
it('handles very large end values', () => {
137+
render(<AnimatedCounter end={999999999} duration={1} />)
138+
expect(screen.getByText('0')).toBeInTheDocument()
139+
})
140+
141+
it('handles zero duration gracefully', () => {
142+
render(<AnimatedCounter end={100} duration={0} />)
143+
expect(screen.getByText('0')).toBeInTheDocument()
144+
})
145+
146+
it('handles negative duration gracefully', () => {
147+
render(<AnimatedCounter end={100} duration={-1} />)
148+
expect(screen.getByText('0')).toBeInTheDocument()
149+
})
150+
})
151+
152+
describe('DOM structure / classNames / styles', () => {
153+
it('renders with correct HTML structure', () => {
154+
render(<AnimatedCounter end={100} duration={2} className="test-class" />)
155+
const element = screen.getByText('0')
156+
expect(element).toBeInTheDocument()
157+
expect(element.tagName).toBe('SPAN')
158+
expect(element).toHaveClass('test-class')
159+
})
160+
161+
it('applies multiple CSS classes when provided', () => {
162+
render(<AnimatedCounter end={100} duration={2} className="class1 class2" />)
163+
const element = screen.getByText('0')
164+
expect(element).toHaveClass('class1', 'class2')
165+
})
166+
167+
it('handles empty className string', () => {
168+
render(<AnimatedCounter end={100} duration={2} className="" />)
169+
const element = screen.getByText('0')
170+
expect(element).toHaveAttribute('class', '')
171+
})
172+
})
173+
174+
describe('Animation behavior', () => {
175+
it('calls requestAnimationFrame during animation', () => {
176+
const requestAnimationFrameSpy = jest.spyOn(window, 'requestAnimationFrame')
177+
render(<AnimatedCounter end={100} duration={1} />)
178+
179+
expect(requestAnimationFrameSpy).toHaveBeenCalled()
180+
})
181+
182+
it('renders final value correctly', () => {
183+
render(<AnimatedCounter end={100} duration={0.1} />)
184+
185+
act(() => {
186+
jest.advanceTimersByTime(100)
187+
})
188+
189+
expect(screen.getByText('100')).toBeInTheDocument()
190+
})
191+
192+
it('updates count correctly', () => {
193+
render(<AnimatedCounter end={10} duration={1} />)
194+
195+
act(() => {
196+
jest.advanceTimersByTime(1000)
197+
})
198+
199+
expect(screen.getByText('10')).toBeInTheDocument()
200+
})
201+
})
202+
203+
describe('Component lifecycle', () => {
204+
it('re-initializes animation when props change', () => {
205+
const { rerender } = render(<AnimatedCounter end={50} duration={1} />)
206+
207+
// Wait for first animation to complete
208+
act(() => {
209+
jest.advanceTimersByTime(1000)
210+
})
211+
212+
expect(screen.getByText('50')).toBeInTheDocument()
213+
214+
// Change props
215+
rerender(<AnimatedCounter end={100} duration={2} />)
216+
217+
// Should show the new end value after animation completes
218+
act(() => {
219+
jest.advanceTimersByTime(2000)
220+
})
221+
222+
expect(screen.getByText('100')).toBeInTheDocument()
223+
})
224+
225+
it('handles rapid prop changes gracefully', () => {
226+
const { rerender } = render(<AnimatedCounter end={10} duration={1} />)
227+
228+
// Rapidly change props
229+
rerender(<AnimatedCounter end={20} duration={1} />)
230+
rerender(<AnimatedCounter end={30} duration={1} />)
231+
rerender(<AnimatedCounter end={40} duration={1} />)
232+
233+
// Should not crash and should render
234+
expect(screen.getByText('0')).toBeInTheDocument()
235+
})
236+
})
237+
238+
describe('Accessibility considerations', () => {
239+
it('renders content that can be read by screen readers', () => {
240+
render(<AnimatedCounter end={100} duration={2} />)
241+
const element = screen.getByText('0')
242+
expect(element).toBeInTheDocument()
243+
expect(element.textContent).toBeTruthy()
244+
})
245+
246+
it('maintains semantic meaning of displayed numbers', () => {
247+
render(<AnimatedCounter end={42} duration={1} />)
248+
const element = screen.getByText('0')
249+
expect(element).toBeInTheDocument()
250+
// The number should be meaningful to screen readers
251+
expect(element.textContent).toMatch(/\d+/)
252+
})
253+
})
254+
255+
describe('Event handling and user interactions', () => {
256+
it('responds to prop changes correctly', () => {
257+
const { rerender } = render(<AnimatedCounter end={100} duration={1} />)
258+
259+
act(() => {
260+
jest.advanceTimersByTime(1000)
261+
})
262+
263+
expect(screen.getByText('100')).toBeInTheDocument()
264+
265+
// Change end value
266+
rerender(<AnimatedCounter end={200} duration={1} />)
267+
268+
// Should show the new end value after animation completes
269+
act(() => {
270+
jest.advanceTimersByTime(1000)
271+
})
272+
273+
expect(screen.getByText('200')).toBeInTheDocument()
274+
})
275+
})
276+
277+
describe('Performance and optimization', () => {
278+
it('does not cause infinite re-renders', () => {
279+
const renderSpy = jest.fn()
280+
const TestWrapper = () => {
281+
renderSpy()
282+
return <AnimatedCounter end={100} duration={1} />
283+
}
284+
285+
render(<TestWrapper />)
286+
287+
act(() => {
288+
jest.advanceTimersByTime(1000)
289+
})
290+
291+
// Should not have excessive render calls
292+
expect(renderSpy).toHaveBeenCalledTimes(1)
293+
})
294+
})
295+
})

0 commit comments

Comments
 (0)