Skip to content

Commit 95af5ad

Browse files
committed
main 🧊 up deps, add use audio test
1 parent 49fd2fe commit 95af5ad

File tree

8 files changed

+813
-527
lines changed

8 files changed

+813
-527
lines changed

‎packages/cli/package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"pretty": "yarn type && yarn lint && yarn format"
4040
},
4141
"dependencies": {
42-
"@antfu/ni": "^26.1.0",
42+
"@antfu/ni": "^27.0.1",
4343
"@siberiacancode/fetches": "^1.13.1",
4444
"chalk": "5.6.2",
4545
"cosmiconfig": "^9.0.0",
@@ -52,10 +52,10 @@
5252
},
5353
"devDependencies": {
5454
"@types/prompts": "^2.4.9",
55-
"@types/yargs": "^17.0.33",
56-
"cross-env": "^10.0.0",
55+
"@types/yargs": "^17.0.34",
56+
"cross-env": "^10.1.0",
5757
"tsup": "^8.5.0",
58-
"tsx": "^4.20.4"
58+
"tsx": "^4.20.6"
5959
},
6060
"lint-staged": {
6161
"*.{js,ts}": [

‎packages/cli/src/add.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ const updateImports = async (filePath: string, config: ConfigSchema) => {
6060
{
6161
regex: /import\s+\{([^}]+)\}\s+from\s+['"](@\/utils[^'"]*)['"]/g,
6262
replacer: (_, imports) =>
63-
`import { ${imports} } from '${config.aliases.utils}'`,
63+
`import {${imports}} from '${config.aliases.utils}'`,
6464
},
6565
{
6666
regex: /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"](\.[^'"]*)['"]/g,
6767
replacer: (_, imports, internalPath) =>
68-
`import { ${imports} } from '${toCase(internalPath, config.case)}'`,
68+
`import {${imports}} from '${toCase(internalPath, config.case)}'`,
6969
},
7070
];
7171

‎packages/core/package.json‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,21 @@
6464
"screenfull": "^6.0.2"
6565
},
6666
"devDependencies": {
67-
"@siberiacancode/vitest": "^2.1.0",
67+
"@siberiacancode/vitest": "^2.2.0",
6868
"@testing-library/dom": "^10.4.1",
6969
"@testing-library/react": "^16.3.0",
70-
"@types/dom-speech-recognition": "^0.0.6",
71-
"@types/react": "^19.1.10",
72-
"@types/react-dom": "^19.1.7",
70+
"@types/dom-speech-recognition": "^0.0.7",
71+
"@types/react": "^19.2.2",
72+
"@types/react-dom": "^19.2.2",
7373
"@types/web-bluetooth": "^0.0.21",
74-
"@vitejs/plugin-react": "^5.0.1",
75-
"core-js": "^3.45.0",
76-
"react": "^19.1.1",
77-
"react-dom": "^19.1.1",
74+
"@vitejs/plugin-react": "^5.1.0",
75+
"core-js": "^3.46.0",
76+
"react": "^19.2.0",
77+
"react-dom": "^19.2.0",
7878
"shx": "^0.4.0",
79-
"vite": "^7.1.3",
79+
"vite": "^7.2.2",
8080
"vite-plugin-dts": "^4.5.4",
81-
"vitest": "^3.2.4"
81+
"vitest": "^4.0.8"
8282
},
8383
"lint-staged": {
8484
"*.{js,ts,tsx}": [

‎packages/core/src/bundle/hooks/useAudio/useAudio.js‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,28 @@ export const useAudio = (src, options = {}) => {
3030
audio.volume = volume;
3131
audio.playbackRate = playbackRate;
3232
audioRef.current = audio;
33-
if (options.immediately) audio.play();
33+
if (options.immediately) {
34+
try {
35+
setPlaying(true);
36+
audio.play();
37+
} catch {
38+
setPlaying(false);
39+
}
40+
}
3441
const onPlay = () => setPlaying(true);
3542
const onPause = () => setPlaying(false);
3643
const onEnded = () => setPlaying(false);
37-
const onTimeUpdate = () => {};
3844
const onVolumeChange = () => setCurrentVolume(audio.volume);
3945
const onRateChange = () => setPlaybackRate(audio.playbackRate);
4046
audio.addEventListener('play', onPlay);
4147
audio.addEventListener('pause', onPause);
4248
audio.addEventListener('ended', onEnded);
43-
audio.addEventListener('timeupdate', onTimeUpdate);
4449
audio.addEventListener('volumechange', onVolumeChange);
4550
audio.addEventListener('ratechange', onRateChange);
4651
return () => {
4752
audio.removeEventListener('play', onPlay);
4853
audio.removeEventListener('pause', onPause);
4954
audio.removeEventListener('ended', onEnded);
50-
audio.removeEventListener('timeupdate', onTimeUpdate);
5155
audio.removeEventListener('volumechange', onVolumeChange);
5256
audio.removeEventListener('ratechange', onRateChange);
5357
audio.pause();
@@ -58,10 +62,12 @@ export const useAudio = (src, options = {}) => {
5862
if (!audioRef.current) return;
5963
audioRef.current.pause();
6064
audioRef.current.currentTime = 0;
65+
setPlaying(false);
6166
};
6267
const play = async (spriteName) => {
6368
if (!audioRef.current) return;
6469
if (options.interrupt) stop();
70+
setPlaying(true);
6571
if (!spriteName || !options.sprite?.[spriteName]) {
6672
await audioRef.current.play();
6773
return;
@@ -79,7 +85,11 @@ export const useAudio = (src, options = {}) => {
7985
};
8086
requestAnimationFrame(checkTime);
8187
};
82-
const pause = () => audioRef.current?.pause();
88+
const pause = () => {
89+
if (!audioRef.current) return;
90+
audioRef.current.pause();
91+
setPlaying(false);
92+
};
8393
const setVolume = (value) => {
8494
if (!audioRef.current) return;
8595
const newVolume = Math.max(0, Math.min(1, value));
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
3+
import { createTrigger, renderHookServer } from '@/tests';
4+
5+
import type { SpriteMap } from './useAudio';
6+
7+
import { useAudio } from './useAudio';
8+
9+
const trigger = createTrigger<string, (event: MessageEvent) => void>();
10+
const mockAudioPlay = vi.fn();
11+
const mockAudioPause = vi.fn();
12+
const mockAudioRemove = vi.fn();
13+
const mockAudioRemoveEventListener = vi.fn();
14+
const mockAudioAddEventListener = vi.fn();
15+
16+
class MockAudio {
17+
play = mockAudioPlay;
18+
pause = mockAudioPause;
19+
remove = mockAudioRemove;
20+
addEventListener = (type: string, callback: (event: MessageEvent) => void) => {
21+
mockAudioAddEventListener(type, callback);
22+
trigger.add(type, callback);
23+
};
24+
removeEventListener = (type: string, callback: (event: MessageEvent) => void) => {
25+
mockAudioRemoveEventListener(type, callback);
26+
if (trigger.get(type) === callback) trigger.delete(type);
27+
};
28+
volume = 0;
29+
playbackRate = 0;
30+
currentTime = 0;
31+
}
32+
33+
afterEach(vi.clearAllMocks);
34+
35+
globalThis.Audio = MockAudio as unknown as typeof Audio;
36+
37+
it('Should use audio', () => {
38+
const { result } = renderHook(() => useAudio('audio.mp3'));
39+
40+
expect(result.current.playing).toBeFalsy();
41+
expect(result.current.volume).toBe(1);
42+
expect(result.current.playbackRate).toBe(1);
43+
expect(result.current.play).toBeTypeOf('function');
44+
expect(result.current.pause).toBeTypeOf('function');
45+
expect(result.current.stop).toBeTypeOf('function');
46+
expect(result.current.setVolume).toBeTypeOf('function');
47+
expect(result.current.changePlaybackRate).toBeTypeOf('function');
48+
});
49+
50+
it('Should use audio on server side', () => {
51+
const { result } = renderHookServer(() => useAudio('audio.mp3'));
52+
53+
expect(result.current.playing).toBeFalsy();
54+
expect(result.current.volume).toBe(1);
55+
expect(result.current.playbackRate).toBe(1);
56+
expect(result.current.play).toBeTypeOf('function');
57+
expect(result.current.pause).toBeTypeOf('function');
58+
expect(result.current.stop).toBeTypeOf('function');
59+
expect(result.current.setVolume).toBeTypeOf('function');
60+
expect(result.current.changePlaybackRate).toBeTypeOf('function');
61+
});
62+
63+
it('Should initialize with custom options', () => {
64+
const options = {
65+
volume: 0.5,
66+
playbackRate: 1.5,
67+
interrupt: true
68+
};
69+
70+
const { result } = renderHook(() => useAudio('audio.mp3', options));
71+
72+
expect(result.current.volume).toBe(0.5);
73+
expect(result.current.playbackRate).toBe(1.5);
74+
});
75+
76+
it('Should play immediately', () => {
77+
const { result } = renderHook(() => useAudio('audio.mp3', { immediately: true }));
78+
79+
expect(result.current.playing).toBeTruthy();
80+
81+
expect(mockAudioPlay).toHaveBeenCalledTimes(1);
82+
});
83+
84+
it('Should play audio', async () => {
85+
const { result } = renderHook(() => useAudio('audio.mp3'));
86+
87+
await act(result.current.play);
88+
89+
expect(mockAudioPlay).toHaveBeenCalledTimes(1);
90+
});
91+
92+
it('Should pause audio', async () => {
93+
const { result } = renderHook(() =>
94+
useAudio('audio.mp3', {
95+
immediately: true
96+
})
97+
);
98+
99+
expect(result.current.playing).toBeTruthy();
100+
101+
await act(result.current.pause);
102+
103+
expect(result.current.playing).toBeFalsy();
104+
expect(mockAudioPause).toHaveBeenCalledTimes(1);
105+
});
106+
107+
it('Should stop audio', async () => {
108+
const { result } = renderHook(() =>
109+
useAudio('audio.mp3', {
110+
immediately: true
111+
})
112+
);
113+
114+
expect(result.current.playing).toBeTruthy();
115+
116+
await act(result.current.stop);
117+
118+
expect(result.current.playing).toBeFalsy();
119+
expect(mockAudioPause).toHaveBeenCalledTimes(1);
120+
});
121+
122+
it('Should set volume', async () => {
123+
const { result } = renderHook(() => useAudio('audio.mp3'));
124+
125+
act(() => result.current.setVolume(0.7));
126+
127+
expect(result.current.volume).toBe(0.7);
128+
});
129+
130+
it('Should clamp volume between 0 and 1', () => {
131+
const { result } = renderHook(() => useAudio('audio.mp3'));
132+
133+
act(() => result.current.setVolume(1.5));
134+
expect(result.current.volume).toBe(1);
135+
136+
act(() => result.current.setVolume(-0.5));
137+
expect(result.current.volume).toBe(0);
138+
});
139+
140+
it('Should change playback rate', () => {
141+
const { result } = renderHook(() => useAudio('audio.mp3'));
142+
143+
act(() => result.current.changePlaybackRate(1.25));
144+
145+
expect(result.current.playbackRate).toBe(1.25);
146+
});
147+
148+
it('Should clamp playback rate between 0.5 and 2', () => {
149+
const { result } = renderHook(() => useAudio('audio.mp3'));
150+
151+
act(() => result.current.changePlaybackRate(3));
152+
expect(result.current.playbackRate).toBe(2);
153+
154+
act(() => result.current.changePlaybackRate(0.25));
155+
expect(result.current.playbackRate).toBe(0.5);
156+
});
157+
158+
it('Should handle sprite playback', async () => {
159+
const sprite = {
160+
intro: [0, 5],
161+
loop: [5, 15]
162+
} as SpriteMap;
163+
164+
const { result } = renderHook(() => useAudio('audio.mp3', { sprite }));
165+
166+
await act(async () => {
167+
await result.current.play('intro');
168+
});
169+
170+
expect(mockAudioPlay).toHaveBeenCalledTimes(1);
171+
});
172+
173+
it('Should recreate audio element when src changes', () => {
174+
const { rerender } = renderHook((src) => useAudio(src, { immediately: true }), {
175+
initialProps: 'audio.mp3'
176+
});
177+
178+
expect(mockAudioPlay).toHaveBeenCalledTimes(1);
179+
180+
rerender('new-audio.mp3');
181+
182+
expect(mockAudioPlay).toHaveBeenCalledTimes(2);
183+
});
184+
185+
it('Should clean up audio element on unmount', () => {
186+
const { unmount } = renderHook(() => useAudio('audio.mp3'));
187+
188+
unmount();
189+
190+
expect(mockAudioRemoveEventListener).toHaveBeenCalledWith('play', expect.any(Function));
191+
expect(mockAudioRemoveEventListener).toHaveBeenCalledWith('pause', expect.any(Function));
192+
expect(mockAudioRemoveEventListener).toHaveBeenCalledWith('ended', expect.any(Function));
193+
expect(mockAudioRemoveEventListener).toHaveBeenCalledWith('volumechange', expect.any(Function));
194+
expect(mockAudioRemoveEventListener).toHaveBeenCalledWith('ratechange', expect.any(Function));
195+
expect(mockAudioPause).toHaveBeenCalled();
196+
expect(mockAudioRemove).toHaveBeenCalled();
197+
});

‎packages/core/src/hooks/useAudio/useAudio.ts‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,27 +75,31 @@ export const useAudio = (src: string, options: UseAudioOptions = {}): UseAudioRe
7575
audio.playbackRate = playbackRate;
7676
audioRef.current = audio;
7777

78-
if (options.immediately) audio.play();
78+
if (options.immediately) {
79+
try {
80+
setPlaying(true);
81+
audio.play();
82+
} catch {
83+
setPlaying(false);
84+
}
85+
}
7986

8087
const onPlay = () => setPlaying(true);
8188
const onPause = () => setPlaying(false);
8289
const onEnded = () => setPlaying(false);
83-
const onTimeUpdate = () => {};
8490
const onVolumeChange = () => setCurrentVolume(audio.volume);
8591
const onRateChange = () => setPlaybackRate(audio.playbackRate);
8692

8793
audio.addEventListener('play', onPlay);
8894
audio.addEventListener('pause', onPause);
8995
audio.addEventListener('ended', onEnded);
90-
audio.addEventListener('timeupdate', onTimeUpdate);
9196
audio.addEventListener('volumechange', onVolumeChange);
9297
audio.addEventListener('ratechange', onRateChange);
9398

9499
return () => {
95100
audio.removeEventListener('play', onPlay);
96101
audio.removeEventListener('pause', onPause);
97102
audio.removeEventListener('ended', onEnded);
98-
audio.removeEventListener('timeupdate', onTimeUpdate);
99103
audio.removeEventListener('volumechange', onVolumeChange);
100104
audio.removeEventListener('ratechange', onRateChange);
101105

@@ -108,12 +112,15 @@ export const useAudio = (src: string, options: UseAudioOptions = {}): UseAudioRe
108112
if (!audioRef.current) return;
109113
audioRef.current.pause();
110114
audioRef.current.currentTime = 0;
115+
setPlaying(false);
111116
};
112117

113118
const play = async (spriteName?: string) => {
114119
if (!audioRef.current) return;
115120
if (options.interrupt) stop();
116121

122+
setPlaying(true);
123+
117124
if (!spriteName || !options.sprite?.[spriteName]) {
118125
await audioRef.current.play();
119126
return;
@@ -137,7 +144,11 @@ export const useAudio = (src: string, options: UseAudioOptions = {}): UseAudioRe
137144
requestAnimationFrame(checkTime);
138145
};
139146

140-
const pause = () => audioRef.current?.pause();
147+
const pause = () => {
148+
if (!audioRef.current) return;
149+
audioRef.current.pause();
150+
setPlaying(false);
151+
};
141152

142153
const setVolume = (value: number) => {
143154
if (!audioRef.current) return;

0 commit comments

Comments
 (0)