Skip to content

Commit 0fc36c8

Browse files
committed
refactor to jest rtl
1 parent 0079ecf commit 0fc36c8

File tree

9 files changed

+1394
-49
lines changed

9 files changed

+1394
-49
lines changed

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@
6767
"@storybook/addon-docs": "^9.0.0",
6868
"@storybook/addon-webpack5-compiler-babel": "^3.0.3",
6969
"@storybook/react-webpack5": "^9.0.0",
70-
"storybook": "^9.0.0",
70+
"@testing-library/dom": "^10.0.0",
71+
"@testing-library/jest-dom": "^6.4.0",
72+
"@testing-library/react": "^16.0.0",
73+
"@testing-library/user-event": "^14.5.2",
7174
"@types/events": "^3.0.0",
7275
"@types/react": "^19",
7376
"@typescript-eslint/eslint-plugin": "^7.18.0",
@@ -82,10 +85,6 @@
8285
"eslint-plugin-prettier": "^4.2.1",
8386
"eslint-plugin-react": "^7.35.0",
8487
"husky": "^9.0.0",
85-
"@testing-library/jest-dom": "^6.4.0",
86-
"@testing-library/react": "^16.0.0",
87-
"@testing-library/user-event": "^14.5.2",
88-
"@testing-library/dom": "^10.0.0",
8988
"jest": "^29.7.0",
9089
"jest-environment-jsdom": "^29.7.0",
9190
"less-plugin-sass2less": "^1.2.0",
@@ -97,7 +96,9 @@
9796
"remark-gfm": "^4.0.0",
9897
"sass": "^1.26.8",
9998
"sass-loader": "^10.2.0",
99+
"semver": "^7.7.2",
100100
"static-server": "^2.2.1",
101+
"storybook": "^9.0.0",
101102
"style-loader": "^4.0.0",
102103
"typescript": "^5.4.0",
103104
"webpack": "^5",

src/CurrentTime.test.js

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,146 @@
11
import React from 'react'
2-
import { render } from '@testing-library/react'
2+
import { render, act } from '@testing-library/react'
33
import CurrentTime from './CurrentTime'
44

5+
// Helper to create a mock HTMLAudioElement-like object
6+
function createMockAudio({ currentTime = 0, duration = 0 } = {}) {
7+
const listeners = {}
8+
const audio = {
9+
currentTime,
10+
duration,
11+
addEventListener: jest.fn((event, cb) => {
12+
listeners[event] = cb
13+
}),
14+
removeEventListener: jest.fn((event) => {
15+
delete listeners[event]
16+
}),
17+
// Utility to trigger an event
18+
dispatch(event) {
19+
if (listeners[event]) {
20+
listeners[event]({ target: audio })
21+
}
22+
},
23+
}
24+
return audio
25+
}
26+
527
describe('CurrentTime component', () => {
6-
it('renders with default current time (placeholder)', () => {
28+
const defaultDisplay = '00:00'
29+
30+
it('renders defaultCurrentTime when no audio provided', () => {
31+
const { container } = render(
32+
<CurrentTime
33+
defaultCurrentTime={defaultDisplay}
34+
isLeftTime={false}
35+
timeFormat="auto"
36+
/>
37+
)
38+
expect(container.textContent).toBe(defaultDisplay)
39+
})
40+
41+
it('displays current playback time (mm:ss) after metadata loaded', () => {
42+
const audio = createMockAudio({ currentTime: 30, duration: 300 })
43+
const { container } = render(
44+
<CurrentTime
45+
audio={audio}
46+
defaultCurrentTime={defaultDisplay}
47+
isLeftTime={false}
48+
timeFormat="auto"
49+
/>
50+
)
51+
// Simulate browser firing loadedmetadata first
52+
act(() => {
53+
audio.dispatch('loadedmetadata')
54+
})
55+
expect(container.textContent).toBe('05:00' === '05:00' ? '00:30' : '00:30') // ensure explicit expected
56+
// Now update currentTime and fire timeupdate
57+
audio.currentTime = 90
58+
act(() => {
59+
audio.dispatch('timeupdate')
60+
})
61+
expect(container.textContent).toBe('01:30')
62+
})
63+
64+
it('displays remaining (left) time when isLeftTime is true', () => {
65+
const audio = createMockAudio({ currentTime: 60, duration: 300 }) // remaining 240s => 04:00
66+
const { container } = render(
67+
<CurrentTime
68+
audio={audio}
69+
defaultCurrentTime={defaultDisplay}
70+
isLeftTime={true}
71+
timeFormat="mm:ss"
72+
/>
73+
)
74+
act(() => {
75+
audio.dispatch('loadedmetadata')
76+
})
77+
expect(container.textContent).toBe('04:00')
78+
// Advance playback
79+
audio.currentTime = 90 // remaining 210 -> 03:30
80+
act(() => {
81+
audio.dispatch('timeupdate')
82+
})
83+
expect(container.textContent).toBe('03:30')
84+
})
85+
86+
it('formats as hh:mm:ss when auto and total duration >= 3600', () => {
87+
const audio = createMockAudio({ currentTime: 100, duration: 3700 }) // remaining 3600 -> 1:00:00
788
const { container } = render(
8-
// @ts-ignore testing JS placeholder
9-
React.createElement(CurrentTime, { defaultCurrentTime: null, isLeftTime: false, timeFormat: 'auto' })
89+
<CurrentTime
90+
audio={audio}
91+
defaultCurrentTime={defaultDisplay}
92+
isLeftTime={true}
93+
timeFormat="auto"
94+
/>
95+
)
96+
act(() => {
97+
audio.dispatch('loadedmetadata')
98+
})
99+
expect(container.textContent).toBe('1:00:00')
100+
})
101+
102+
it('honors explicit hh:mm:ss formatting', () => {
103+
const audio = createMockAudio({ currentTime: 3661, duration: 5400 }) // 1:01:01
104+
const { container } = render(
105+
<CurrentTime
106+
audio={audio}
107+
defaultCurrentTime={defaultDisplay}
108+
isLeftTime={false}
109+
timeFormat="hh:mm:ss"
110+
/>
111+
)
112+
act(() => {
113+
audio.dispatch('loadedmetadata')
114+
})
115+
expect(container.textContent).toBe('1:01:01')
116+
})
117+
118+
it('adds audio event listeners only once across re-renders', () => {
119+
const audio = createMockAudio({ currentTime: 0, duration: 100 })
120+
const { rerender } = render(
121+
<CurrentTime
122+
audio={audio}
123+
defaultCurrentTime={defaultDisplay}
124+
isLeftTime={false}
125+
timeFormat="auto"
126+
/>
127+
)
128+
// Re-render with different prop that triggers componentDidUpdate
129+
rerender(
130+
<CurrentTime
131+
audio={audio}
132+
defaultCurrentTime={defaultDisplay}
133+
isLeftTime={false}
134+
timeFormat="mm:ss" // change format
135+
/>
10136
)
11-
expect(container).toBeTruthy()
137+
// Only two listeners (timeupdate & loadedmetadata) should be registered once
138+
expect(audio.addEventListener).toHaveBeenCalledTimes(2)
139+
// Fire timeupdate to ensure still functional
140+
audio.currentTime = 10
141+
act(() => {
142+
audio.dispatch('timeupdate')
143+
})
12144
})
13145
})
14146

src/Duration.test.js

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,113 @@
11
import React from 'react'
2-
import { render } from '@testing-library/react'
2+
import { render, act } from '@testing-library/react'
33
import Duration from './Duration'
44

5+
function createMockAudio({ duration = 0 } = {}) {
6+
const listeners = {}
7+
const audio = {
8+
duration,
9+
addEventListener: jest.fn((event, cb) => {
10+
listeners[event] = cb
11+
}),
12+
removeEventListener: jest.fn((event) => {
13+
delete listeners[event]
14+
}),
15+
dispatch(event) {
16+
if (listeners[event]) listeners[event]({ target: audio })
17+
},
18+
}
19+
return audio
20+
}
21+
522
describe('Duration component', () => {
6-
it('renders with null default (no crash)', () => {
23+
const defaultDisplay = '00:00'
24+
25+
it('renders defaultDuration when no audio provided', () => {
26+
const { container } = render(
27+
<Duration defaultDuration={defaultDisplay} timeFormat="auto" />
28+
)
29+
expect(container.textContent).toBe(defaultDisplay)
30+
})
31+
32+
it('displays duration (mm:ss) with audio and format auto (< 1h)', () => {
33+
const audio = createMockAudio({ duration: 305 }) // 5:05
34+
const { container } = render(
35+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="auto" />
36+
)
37+
// Initial render already computes value
38+
expect(container.textContent).toBe('05:05')
39+
})
40+
41+
it('displays duration (hh:mm:ss) when auto and >= 3600', () => {
42+
const audio = createMockAudio({ duration: 3661 }) // 1:01:01
43+
const { container } = render(
44+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="auto" />
45+
)
46+
expect(container.textContent).toBe('1:01:01')
47+
})
48+
49+
it('honors explicit mm:ss', () => {
50+
const audio = createMockAudio({ duration: 3661 })
51+
const { container } = render(
52+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="mm:ss" />
53+
)
54+
// Even though >= 1h, should truncate to minutes only
55+
expect(container.textContent).toBe('61:01')
56+
})
57+
58+
it('honors explicit hh:mm:ss', () => {
59+
const audio = createMockAudio({ duration: 59 })
60+
const { container } = render(
61+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="hh:mm:ss" />
62+
)
63+
expect(container.textContent).toBe('0:00:59')
64+
})
65+
66+
it('updates on durationchange event', () => {
67+
const audio = createMockAudio({ duration: 10 })
768
const { container } = render(
8-
// @ts-ignore testing JS placeholder
9-
React.createElement(Duration, { defaultDuration: null, timeFormat: 'auto' })
69+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="mm:ss" />
70+
)
71+
expect(container.textContent).toBe('00:10')
72+
audio.duration = 25
73+
act(() => {
74+
audio.dispatch('durationchange')
75+
})
76+
expect(container.textContent).toBe('00:25')
77+
})
78+
79+
it('falls back to defaultDuration after event when initial formatter returns null (NaN)', () => {
80+
const audio = createMockAudio({ duration: NaN })
81+
const { container } = render(
82+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="auto" />
83+
)
84+
// Initial state stores null directly (renders empty string)
85+
expect(container.textContent).toBe('')
86+
act(() => {
87+
audio.dispatch('durationchange')
88+
})
89+
expect(container.textContent).toBe(defaultDisplay)
90+
})
91+
92+
it('adds listeners only once across re-renders', () => {
93+
const audio = createMockAudio({ duration: 100 })
94+
const { rerender } = render(
95+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="mm:ss" />
96+
)
97+
// Re-render with different timeFormat
98+
rerender(
99+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="hh:mm:ss" />
100+
)
101+
expect(audio.addEventListener).toHaveBeenCalledTimes(2) // durationchange + abort
102+
})
103+
104+
it('removes listeners on unmount', () => {
105+
const audio = createMockAudio({ duration: 50 })
106+
const { unmount } = render(
107+
<Duration audio={audio} defaultDuration={defaultDisplay} timeFormat="mm:ss" />
10108
)
11-
expect(container).toBeTruthy()
109+
unmount()
110+
expect(audio.removeEventListener).toHaveBeenCalledTimes(2)
12111
})
13112
})
14113

0 commit comments

Comments
 (0)