@zentauri-ui/zentauri-components 2.1.4 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/cli/cli.integration.test.ts +44 -2
- package/cli/index.mjs +134 -28
- package/cli/index.test.ts +180 -0
- package/cli/props.json +15180 -0
- package/cli/props.test.ts +80 -0
- package/cli/registry.json +2 -0
- package/dist/chunk-3W2UUKWP.js +19 -0
- package/dist/{chunk-D2GISTDL.js.map → chunk-3W2UUKWP.js.map} +1 -1
- package/dist/{chunk-BL6UVCV7.mjs → chunk-A4IB3C23.mjs} +16 -7
- package/dist/chunk-A4IB3C23.mjs.map +1 -0
- package/dist/{chunk-WBZKMSXW.mjs → chunk-CHI6MBTI.mjs} +3 -3
- package/dist/{chunk-WBZKMSXW.mjs.map → chunk-CHI6MBTI.mjs.map} +1 -1
- package/dist/chunk-COCPCZMR.mjs +77 -0
- package/dist/chunk-COCPCZMR.mjs.map +1 -0
- package/dist/chunk-PG7LQVU6.js +86 -0
- package/dist/chunk-PG7LQVU6.js.map +1 -0
- package/dist/{chunk-RENXBUZY.js → chunk-QE7OJW4J.js} +6 -6
- package/dist/{chunk-RENXBUZY.js.map → chunk-QE7OJW4J.js.map} +1 -1
- package/dist/{chunk-NZSZE36T.js → chunk-VA6SB6NN.js} +16 -7
- package/dist/{chunk-BL6UVCV7.mjs.map → chunk-VA6SB6NN.js.map} +1 -1
- package/dist/{chunk-PAG5CTLN.mjs → chunk-WWKAJHIV.mjs} +3 -3
- package/dist/{chunk-PAG5CTLN.mjs.map → chunk-WWKAJHIV.mjs.map} +1 -1
- package/dist/design-system/audio-player.d.ts +61 -0
- package/dist/design-system/audio-player.d.ts.map +1 -0
- package/dist/design-system/facade.js +8 -7
- package/dist/design-system/facade.js.map +1 -1
- package/dist/design-system/facade.mjs +7 -6
- package/dist/design-system/facade.mjs.map +1 -1
- package/dist/design-system/index.d.ts +1 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/ui/audio-player/audio-player-base.d.ts +20 -0
- package/dist/ui/audio-player/audio-player-base.d.ts.map +1 -0
- package/dist/ui/audio-player/audio-player.d.ts +6 -0
- package/dist/ui/audio-player/audio-player.d.ts.map +1 -0
- package/dist/ui/audio-player/index.d.ts +5 -0
- package/dist/ui/audio-player/index.d.ts.map +1 -0
- package/dist/ui/audio-player/types.d.ts +44 -0
- package/dist/ui/audio-player/types.d.ts.map +1 -0
- package/dist/ui/audio-player/variants.d.ts +12 -0
- package/dist/ui/audio-player/variants.d.ts.map +1 -0
- package/dist/ui/audio-player.js +556 -0
- package/dist/ui/audio-player.js.map +1 -0
- package/dist/ui/audio-player.mjs +545 -0
- package/dist/ui/audio-player.mjs.map +1 -0
- package/dist/ui/buttons/animated.js +10 -9
- package/dist/ui/buttons/animated.js.map +1 -1
- package/dist/ui/buttons/animated.mjs +8 -7
- package/dist/ui/buttons/animated.mjs.map +1 -1
- package/dist/ui/buttons.js +11 -10
- package/dist/ui/buttons.mjs +9 -8
- package/dist/ui/dynamic-stepper.js +20 -19
- package/dist/ui/dynamic-stepper.js.map +1 -1
- package/dist/ui/dynamic-stepper.mjs +9 -8
- package/dist/ui/dynamic-stepper.mjs.map +1 -1
- package/dist/ui/pagination.js +16 -15
- package/dist/ui/pagination.js.map +1 -1
- package/dist/ui/pagination.mjs +8 -7
- package/dist/ui/pagination.mjs.map +1 -1
- package/package.json +5 -2
- package/src/design-system/audio-player.ts +109 -0
- package/src/design-system/index.ts +1 -0
- package/src/ui/audio-player/audio-player-base.tsx +557 -0
- package/src/ui/audio-player/audio-player.test.tsx +485 -0
- package/src/ui/audio-player/audio-player.tsx +8 -0
- package/src/ui/audio-player/index.ts +24 -0
- package/src/ui/audio-player/types.ts +57 -0
- package/src/ui/audio-player/variants.ts +43 -0
- package/dist/chunk-D2GISTDL.js +0 -19
- package/dist/chunk-NZSZE36T.js.map +0 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen, act } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { AudioPlayer } from "./audio-player";
|
|
7
|
+
import {
|
|
8
|
+
AudioPlayerBase,
|
|
9
|
+
AudioPlayerProgress,
|
|
10
|
+
AudioPlayerTime,
|
|
11
|
+
AudioPlayerVolume,
|
|
12
|
+
useAudioPlayer,
|
|
13
|
+
} from "./audio-player-base";
|
|
14
|
+
|
|
15
|
+
// jsdom does not implement HTMLMediaElement playback — stub the methods.
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
window.HTMLMediaElement.prototype.play = vi.fn().mockResolvedValue(undefined);
|
|
18
|
+
window.HTMLMediaElement.prototype.pause = vi.fn();
|
|
19
|
+
Object.defineProperty(window.HTMLMediaElement.prototype, "duration", {
|
|
20
|
+
configurable: true,
|
|
21
|
+
get: () => 120,
|
|
22
|
+
});
|
|
23
|
+
// jsdom does not implement pointer capture — stub to avoid unhandled errors
|
|
24
|
+
// when userEvent clicks to focus a slider before firing keyboard events.
|
|
25
|
+
HTMLElement.prototype.setPointerCapture = vi.fn();
|
|
26
|
+
HTMLElement.prototype.releasePointerCapture = vi.fn();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Helper: a minimal player with all sub-components and a test-driven controls div.
|
|
30
|
+
function FullPlayer({
|
|
31
|
+
src = "test.mp3",
|
|
32
|
+
...rest
|
|
33
|
+
}: Partial<Parameters<typeof AudioPlayerBase>[0]>) {
|
|
34
|
+
return (
|
|
35
|
+
<AudioPlayerBase src={src} {...rest}>
|
|
36
|
+
<AudioPlayerProgress />
|
|
37
|
+
<AudioPlayerTime />
|
|
38
|
+
<AudioPlayerVolume />
|
|
39
|
+
</AudioPlayerBase>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper: renders a component that reads from context so we can inspect state.
|
|
44
|
+
function ContextInspector() {
|
|
45
|
+
const ctx = useAudioPlayer();
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<span data-testid="isPlaying">{String(ctx.isPlaying)}</span>
|
|
49
|
+
<span data-testid="currentTime">{ctx.currentTime}</span>
|
|
50
|
+
<span data-testid="duration">{ctx.duration}</span>
|
|
51
|
+
<span data-testid="progress">{ctx.progress}</span>
|
|
52
|
+
<span data-testid="volume">{ctx.volume}</span>
|
|
53
|
+
<span data-testid="muted">{String(ctx.muted)}</span>
|
|
54
|
+
<button data-testid="play" onClick={ctx.play}>
|
|
55
|
+
play
|
|
56
|
+
</button>
|
|
57
|
+
<button data-testid="pause" onClick={ctx.pause}>
|
|
58
|
+
pause
|
|
59
|
+
</button>
|
|
60
|
+
<button data-testid="toggle" onClick={ctx.toggle}>
|
|
61
|
+
toggle
|
|
62
|
+
</button>
|
|
63
|
+
<button data-testid="reset" onClick={ctx.reset}>
|
|
64
|
+
reset
|
|
65
|
+
</button>
|
|
66
|
+
<button data-testid="seek" onClick={() => ctx.seek(30)}>
|
|
67
|
+
seek
|
|
68
|
+
</button>
|
|
69
|
+
<button data-testid="seekPct" onClick={() => ctx.seekByPercent(50)}>
|
|
70
|
+
seekPct
|
|
71
|
+
</button>
|
|
72
|
+
<button data-testid="setVol" onClick={() => ctx.setVolume(0.5)}>
|
|
73
|
+
setVol
|
|
74
|
+
</button>
|
|
75
|
+
<button data-testid="toggleMute" onClick={ctx.toggleMute}>
|
|
76
|
+
toggleMute
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("AudioPlayer", () => {
|
|
83
|
+
it("exposes displayName on all parts", () => {
|
|
84
|
+
expect(AudioPlayer.displayName).toBe("AudioPlayer");
|
|
85
|
+
expect(AudioPlayerBase.displayName).toBe("AudioPlayer");
|
|
86
|
+
expect(AudioPlayerProgress.displayName).toBe("AudioPlayerProgress");
|
|
87
|
+
expect(AudioPlayerTime.displayName).toBe("AudioPlayerTime");
|
|
88
|
+
expect(AudioPlayerVolume.displayName).toBe("AudioPlayerVolume");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("stamps data-slot on root", () => {
|
|
92
|
+
render(<FullPlayer />);
|
|
93
|
+
expect(document.querySelector('[data-slot="audio-player"]')).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("renders a hidden audio element with the given src", () => {
|
|
97
|
+
render(<FullPlayer src="song.mp3" />);
|
|
98
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
99
|
+
expect(audio).toBeTruthy();
|
|
100
|
+
expect(audio.src).toContain("song.mp3");
|
|
101
|
+
expect(audio.className).toContain("hidden");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("applies appearance, size, shape classes via variants", () => {
|
|
105
|
+
render(<FullPlayer appearance="blue" size="lg" shape="pill" />);
|
|
106
|
+
const root = document.querySelector(
|
|
107
|
+
'[data-slot="audio-player"]',
|
|
108
|
+
) as HTMLElement;
|
|
109
|
+
expect(root.className).toMatch(/--audio-fill/);
|
|
110
|
+
expect(root.className).toContain("p-5");
|
|
111
|
+
expect(root.className).toContain("text-base");
|
|
112
|
+
expect(root.className).toContain("gap-4");
|
|
113
|
+
expect(root.className).toContain("rounded-3xl");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("forwards ref to the root div", () => {
|
|
117
|
+
const ref = createRef<HTMLDivElement>();
|
|
118
|
+
render(<AudioPlayerBase ref={ref} src="test.mp3" />);
|
|
119
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
120
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("audio-player");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("useAudioPlayer", () => {
|
|
124
|
+
it("throws when used outside AudioPlayer", () => {
|
|
125
|
+
const consoleError = vi
|
|
126
|
+
.spyOn(console, "error")
|
|
127
|
+
.mockImplementation(() => {});
|
|
128
|
+
expect(() => render(<AudioPlayerProgress />)).toThrow(
|
|
129
|
+
"useAudioPlayer must be used within <AudioPlayer>",
|
|
130
|
+
);
|
|
131
|
+
consoleError.mockRestore();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("AudioPlayerProgress", () => {
|
|
136
|
+
it("stamps data-slot and has slider role", () => {
|
|
137
|
+
render(<FullPlayer />);
|
|
138
|
+
const slider = screen.getByRole("slider", { name: "Audio progress" });
|
|
139
|
+
expect(slider).toBeTruthy();
|
|
140
|
+
expect(slider.getAttribute("data-slot")).toBe("audio-player-progress");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("has correct aria attributes at 0%", () => {
|
|
144
|
+
render(<FullPlayer />);
|
|
145
|
+
const slider = screen.getByRole("slider", { name: "Audio progress" });
|
|
146
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("0");
|
|
147
|
+
expect(slider.getAttribute("aria-valuemin")).toBe("0");
|
|
148
|
+
expect(slider.getAttribute("aria-valuemax")).toBe("100");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("stamps data-slot on the bar", () => {
|
|
152
|
+
render(<FullPlayer />);
|
|
153
|
+
expect(
|
|
154
|
+
document.querySelector('[data-slot="audio-player-bar"]'),
|
|
155
|
+
).toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("responds to ArrowRight key to seek forward", async () => {
|
|
159
|
+
const user = userEvent.setup();
|
|
160
|
+
render(
|
|
161
|
+
<AudioPlayerBase src="test.mp3">
|
|
162
|
+
<ContextInspector />
|
|
163
|
+
<AudioPlayerProgress />
|
|
164
|
+
</AudioPlayerBase>,
|
|
165
|
+
);
|
|
166
|
+
const slider = screen.getByRole("slider", { name: "Audio progress" });
|
|
167
|
+
await user.type(slider, "{ArrowRight}");
|
|
168
|
+
// jsdom does not fire timeupdate automatically — dispatch it so React state updates.
|
|
169
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
170
|
+
act(() => {
|
|
171
|
+
audio.dispatchEvent(new Event("timeupdate"));
|
|
172
|
+
});
|
|
173
|
+
// seekByPercent is guarded — duration is 120 (finite > 0); progress goes 0 → 1%
|
|
174
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("1");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("responds to ArrowLeft key", async () => {
|
|
178
|
+
const user = userEvent.setup();
|
|
179
|
+
render(
|
|
180
|
+
<AudioPlayerBase src="test.mp3">
|
|
181
|
+
<AudioPlayerProgress />
|
|
182
|
+
</AudioPlayerBase>,
|
|
183
|
+
);
|
|
184
|
+
const slider = screen.getByRole("slider", { name: "Audio progress" });
|
|
185
|
+
// at 0%, ArrowLeft clamps to 0 — valuenow stays 0
|
|
186
|
+
await user.type(slider, "{ArrowLeft}");
|
|
187
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("0");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("responds to Home and End keys", async () => {
|
|
191
|
+
const user = userEvent.setup();
|
|
192
|
+
render(
|
|
193
|
+
<AudioPlayerBase src="test.mp3">
|
|
194
|
+
<AudioPlayerProgress />
|
|
195
|
+
</AudioPlayerBase>,
|
|
196
|
+
);
|
|
197
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
198
|
+
const slider = screen.getByRole("slider", { name: "Audio progress" });
|
|
199
|
+
await user.type(slider, "{End}");
|
|
200
|
+
act(() => {
|
|
201
|
+
audio.dispatchEvent(new Event("timeupdate"));
|
|
202
|
+
});
|
|
203
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("100");
|
|
204
|
+
await user.type(slider, "{Home}");
|
|
205
|
+
act(() => {
|
|
206
|
+
audio.dispatchEvent(new Event("timeupdate"));
|
|
207
|
+
});
|
|
208
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("0");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("AudioPlayerTime", () => {
|
|
213
|
+
it("stamps data-slot", () => {
|
|
214
|
+
render(<FullPlayer />);
|
|
215
|
+
expect(
|
|
216
|
+
document.querySelector('[data-slot="audio-player-time"]'),
|
|
217
|
+
).toBeTruthy();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("renders 0:00 / 0:00 before metadata loads", () => {
|
|
221
|
+
Object.defineProperty(window.HTMLMediaElement.prototype, "duration", {
|
|
222
|
+
configurable: true,
|
|
223
|
+
get: () => NaN,
|
|
224
|
+
});
|
|
225
|
+
render(<FullPlayer />);
|
|
226
|
+
const timeEl = document.querySelector('[data-slot="audio-player-time"]')!;
|
|
227
|
+
expect(timeEl.textContent).toContain("0:00");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("accepts a custom format function", () => {
|
|
231
|
+
render(
|
|
232
|
+
<AudioPlayerBase src="test.mp3">
|
|
233
|
+
<AudioPlayerTime format={() => "custom"} />
|
|
234
|
+
</AudioPlayerBase>,
|
|
235
|
+
);
|
|
236
|
+
const timeEl = document.querySelector('[data-slot="audio-player-time"]')!;
|
|
237
|
+
expect(timeEl.textContent).toContain("custom");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("AudioPlayerVolume", () => {
|
|
242
|
+
it("stamps data-slot", () => {
|
|
243
|
+
render(<FullPlayer />);
|
|
244
|
+
expect(
|
|
245
|
+
document.querySelector('[data-slot="audio-player-volume"]'),
|
|
246
|
+
).toBeTruthy();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("has a slider with correct aria attributes", () => {
|
|
250
|
+
render(<FullPlayer />);
|
|
251
|
+
const slider = screen.getByRole("slider", { name: "Volume" });
|
|
252
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("100");
|
|
253
|
+
expect(slider.getAttribute("aria-valuemin")).toBe("0");
|
|
254
|
+
expect(slider.getAttribute("aria-valuemax")).toBe("100");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("has a mute toggle button", () => {
|
|
258
|
+
render(<FullPlayer />);
|
|
259
|
+
expect(screen.getByRole("button", { name: /mute/i })).toBeTruthy();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("responds to ArrowLeft key to decrease volume", async () => {
|
|
263
|
+
const user = userEvent.setup();
|
|
264
|
+
render(
|
|
265
|
+
<AudioPlayerBase src="test.mp3">
|
|
266
|
+
<AudioPlayerVolume />
|
|
267
|
+
</AudioPlayerBase>,
|
|
268
|
+
);
|
|
269
|
+
const slider = screen.getByRole("slider", { name: "Volume" });
|
|
270
|
+
await user.type(slider, "{ArrowLeft}");
|
|
271
|
+
expect(Number(slider.getAttribute("aria-valuenow"))).toBeLessThan(100);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("responds to ArrowRight key to increase volume", async () => {
|
|
275
|
+
const user = userEvent.setup();
|
|
276
|
+
render(
|
|
277
|
+
<AudioPlayerBase src="test.mp3">
|
|
278
|
+
<AudioPlayerVolume />
|
|
279
|
+
</AudioPlayerBase>,
|
|
280
|
+
);
|
|
281
|
+
const slider = screen.getByRole("slider", { name: "Volume" });
|
|
282
|
+
await user.type(slider, "{Home}");
|
|
283
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("0");
|
|
284
|
+
await user.type(slider, "{ArrowRight}");
|
|
285
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("5");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("responds to Home and End keys", async () => {
|
|
289
|
+
const user = userEvent.setup();
|
|
290
|
+
render(
|
|
291
|
+
<AudioPlayerBase src="test.mp3">
|
|
292
|
+
<AudioPlayerVolume />
|
|
293
|
+
</AudioPlayerBase>,
|
|
294
|
+
);
|
|
295
|
+
const slider = screen.getByRole("slider", { name: "Volume" });
|
|
296
|
+
await user.type(slider, "{Home}");
|
|
297
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("0");
|
|
298
|
+
await user.type(slider, "{End}");
|
|
299
|
+
expect(slider.getAttribute("aria-valuenow")).toBe("100");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("shows mute icon after clicking the mute button", async () => {
|
|
303
|
+
const user = userEvent.setup();
|
|
304
|
+
render(
|
|
305
|
+
<AudioPlayerBase src="test.mp3">
|
|
306
|
+
<AudioPlayerVolume />
|
|
307
|
+
</AudioPlayerBase>,
|
|
308
|
+
);
|
|
309
|
+
const muteBtn = screen.getByRole("button", { name: /mute/i });
|
|
310
|
+
await user.click(muteBtn);
|
|
311
|
+
expect(screen.getByRole("button", { name: /unmute/i })).toBeTruthy();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("context controls via ContextInspector", () => {
|
|
316
|
+
it("play calls HTMLMediaElement.play", async () => {
|
|
317
|
+
const user = userEvent.setup();
|
|
318
|
+
render(
|
|
319
|
+
<AudioPlayerBase src="test.mp3">
|
|
320
|
+
<ContextInspector />
|
|
321
|
+
</AudioPlayerBase>,
|
|
322
|
+
);
|
|
323
|
+
await user.click(screen.getByTestId("play"));
|
|
324
|
+
expect(window.HTMLMediaElement.prototype.play).toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("pause calls HTMLMediaElement.pause", async () => {
|
|
328
|
+
const user = userEvent.setup();
|
|
329
|
+
render(
|
|
330
|
+
<AudioPlayerBase src="test.mp3">
|
|
331
|
+
<ContextInspector />
|
|
332
|
+
</AudioPlayerBase>,
|
|
333
|
+
);
|
|
334
|
+
await user.click(screen.getByTestId("pause"));
|
|
335
|
+
expect(window.HTMLMediaElement.prototype.pause).toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("toggle calls play when paused", async () => {
|
|
339
|
+
const user = userEvent.setup();
|
|
340
|
+
render(
|
|
341
|
+
<AudioPlayerBase src="test.mp3">
|
|
342
|
+
<ContextInspector />
|
|
343
|
+
</AudioPlayerBase>,
|
|
344
|
+
);
|
|
345
|
+
await user.click(screen.getByTestId("toggle"));
|
|
346
|
+
expect(window.HTMLMediaElement.prototype.play).toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("reset calls pause and sets currentTime to 0", async () => {
|
|
350
|
+
const user = userEvent.setup();
|
|
351
|
+
render(
|
|
352
|
+
<AudioPlayerBase src="test.mp3">
|
|
353
|
+
<ContextInspector />
|
|
354
|
+
</AudioPlayerBase>,
|
|
355
|
+
);
|
|
356
|
+
await user.click(screen.getByTestId("reset"));
|
|
357
|
+
expect(window.HTMLMediaElement.prototype.pause).toHaveBeenCalled();
|
|
358
|
+
expect(screen.getByTestId("currentTime").textContent).toBe("0");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("seek sets audio.currentTime", async () => {
|
|
362
|
+
const user = userEvent.setup();
|
|
363
|
+
render(
|
|
364
|
+
<AudioPlayerBase src="test.mp3">
|
|
365
|
+
<ContextInspector />
|
|
366
|
+
</AudioPlayerBase>,
|
|
367
|
+
);
|
|
368
|
+
await user.click(screen.getByTestId("seek"));
|
|
369
|
+
// Seek to 30s out of 120s — audio.currentTime is set but React state
|
|
370
|
+
// only updates via the timeupdate event (not fired in jsdom); we just
|
|
371
|
+
// assert no throw occurs and the audio element was targeted.
|
|
372
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
373
|
+
expect(audio.currentTime).toBe(30);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("seekByPercent sets audio.currentTime proportionally", async () => {
|
|
377
|
+
const user = userEvent.setup();
|
|
378
|
+
render(
|
|
379
|
+
<AudioPlayerBase src="test.mp3">
|
|
380
|
+
<ContextInspector />
|
|
381
|
+
</AudioPlayerBase>,
|
|
382
|
+
);
|
|
383
|
+
await user.click(screen.getByTestId("seekPct"));
|
|
384
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
385
|
+
expect(audio.currentTime).toBe(60); // 50% of 120
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("setVolume updates audio.volume", async () => {
|
|
389
|
+
const user = userEvent.setup();
|
|
390
|
+
render(
|
|
391
|
+
<AudioPlayerBase src="test.mp3">
|
|
392
|
+
<ContextInspector />
|
|
393
|
+
</AudioPlayerBase>,
|
|
394
|
+
);
|
|
395
|
+
await user.click(screen.getByTestId("setVol"));
|
|
396
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
397
|
+
expect(audio.volume).toBe(0.5);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("toggleMute flips audio.muted", async () => {
|
|
401
|
+
const user = userEvent.setup();
|
|
402
|
+
render(
|
|
403
|
+
<AudioPlayerBase src="test.mp3">
|
|
404
|
+
<ContextInspector />
|
|
405
|
+
</AudioPlayerBase>,
|
|
406
|
+
);
|
|
407
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
408
|
+
expect(audio.muted).toBe(false);
|
|
409
|
+
await user.click(screen.getByTestId("toggleMute"));
|
|
410
|
+
expect(audio.muted).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("onPlay / onPause callbacks fire via simulated audio events", async () => {
|
|
414
|
+
const onPlay = vi.fn();
|
|
415
|
+
const onPause = vi.fn();
|
|
416
|
+
render(
|
|
417
|
+
<AudioPlayerBase src="test.mp3" onPlay={onPlay} onPause={onPause}>
|
|
418
|
+
<ContextInspector />
|
|
419
|
+
</AudioPlayerBase>,
|
|
420
|
+
);
|
|
421
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
422
|
+
act(() => {
|
|
423
|
+
audio.dispatchEvent(new Event("play"));
|
|
424
|
+
});
|
|
425
|
+
expect(onPlay).toHaveBeenCalledTimes(1);
|
|
426
|
+
act(() => {
|
|
427
|
+
audio.dispatchEvent(new Event("pause"));
|
|
428
|
+
});
|
|
429
|
+
expect(onPause).toHaveBeenCalledTimes(1);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("onEnded callback fires and isPlaying becomes false", () => {
|
|
433
|
+
const onEnded = vi.fn();
|
|
434
|
+
render(
|
|
435
|
+
<AudioPlayerBase src="test.mp3" onEnded={onEnded}>
|
|
436
|
+
<ContextInspector />
|
|
437
|
+
</AudioPlayerBase>,
|
|
438
|
+
);
|
|
439
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
440
|
+
act(() => {
|
|
441
|
+
audio.dispatchEvent(new Event("ended"));
|
|
442
|
+
});
|
|
443
|
+
expect(onEnded).toHaveBeenCalledTimes(1);
|
|
444
|
+
expect(screen.getByTestId("isPlaying").textContent).toBe("false");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("durationchange updates duration state", () => {
|
|
448
|
+
render(
|
|
449
|
+
<AudioPlayerBase src="test.mp3">
|
|
450
|
+
<ContextInspector />
|
|
451
|
+
</AudioPlayerBase>,
|
|
452
|
+
);
|
|
453
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
454
|
+
act(() => {
|
|
455
|
+
audio.dispatchEvent(new Event("durationchange"));
|
|
456
|
+
});
|
|
457
|
+
expect(Number(screen.getByTestId("duration").textContent)).toBe(120);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("src change resets state", () => {
|
|
462
|
+
it("resets isPlaying and currentTime when src prop changes", async () => {
|
|
463
|
+
const { rerender } = render(
|
|
464
|
+
<AudioPlayerBase src="a.mp3">
|
|
465
|
+
<ContextInspector />
|
|
466
|
+
</AudioPlayerBase>,
|
|
467
|
+
);
|
|
468
|
+
// simulate playing
|
|
469
|
+
const audio = document.querySelector("audio") as HTMLAudioElement;
|
|
470
|
+
act(() => {
|
|
471
|
+
audio.dispatchEvent(new Event("play"));
|
|
472
|
+
});
|
|
473
|
+
expect(screen.getByTestId("isPlaying").textContent).toBe("true");
|
|
474
|
+
|
|
475
|
+
rerender(
|
|
476
|
+
<AudioPlayerBase src="b.mp3">
|
|
477
|
+
<ContextInspector />
|
|
478
|
+
</AudioPlayerBase>,
|
|
479
|
+
);
|
|
480
|
+
expect(screen.getByTestId("isPlaying").textContent).toBe("false");
|
|
481
|
+
expect(screen.getByTestId("currentTime").textContent).toBe("0");
|
|
482
|
+
expect(screen.getByTestId("duration").textContent).toBe("0");
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export { AudioPlayer } from "./audio-player";
|
|
4
|
+
export {
|
|
5
|
+
AudioPlayerBase,
|
|
6
|
+
AudioPlayerProgress,
|
|
7
|
+
AudioPlayerTime,
|
|
8
|
+
AudioPlayerVolume,
|
|
9
|
+
useAudioPlayer,
|
|
10
|
+
} from "./audio-player-base";
|
|
11
|
+
export type {
|
|
12
|
+
AudioPlayerProps,
|
|
13
|
+
AudioPlayerProgressProps,
|
|
14
|
+
AudioPlayerTimeProps,
|
|
15
|
+
AudioPlayerVolumeProps,
|
|
16
|
+
AudioPlayerCtx,
|
|
17
|
+
AudioPlayerVariantProps,
|
|
18
|
+
} from "./types";
|
|
19
|
+
export {
|
|
20
|
+
audioPlayerVariants,
|
|
21
|
+
audioPlayerTrackVariants,
|
|
22
|
+
audioPlayerBarVariants,
|
|
23
|
+
audioPlayerTimeVariants,
|
|
24
|
+
} from "./variants";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { audioPlayerVariants } from "./variants";
|
|
5
|
+
|
|
6
|
+
export type AudioPlayerVariantProps = VariantProps<typeof audioPlayerVariants>;
|
|
7
|
+
|
|
8
|
+
export type AudioPlayerProps = AudioPlayerVariantProps &
|
|
9
|
+
Omit<ComponentPropsWithRef<"div">, "children"> & {
|
|
10
|
+
src: string;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
autoPlay?: boolean;
|
|
13
|
+
loop?: boolean;
|
|
14
|
+
onEnded?: () => void;
|
|
15
|
+
onPlay?: () => void;
|
|
16
|
+
onPause?: () => void;
|
|
17
|
+
onTimeUpdate?: (currentTime: number, duration: number) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type AudioPlayerProgressProps = Omit<
|
|
21
|
+
ComponentPropsWithRef<"div">,
|
|
22
|
+
"children"
|
|
23
|
+
> & {
|
|
24
|
+
className?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type AudioPlayerTimeProps = {
|
|
28
|
+
className?: string;
|
|
29
|
+
format?: (seconds: number) => string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type AudioPlayerVolumeProps = Omit<
|
|
33
|
+
ComponentPropsWithRef<"div">,
|
|
34
|
+
"children"
|
|
35
|
+
> & {
|
|
36
|
+
className?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AudioPlayerCtx = {
|
|
40
|
+
isPlaying: boolean;
|
|
41
|
+
currentTime: number;
|
|
42
|
+
duration: number;
|
|
43
|
+
progress: number;
|
|
44
|
+
volume: number;
|
|
45
|
+
muted: boolean;
|
|
46
|
+
play: () => void;
|
|
47
|
+
pause: () => void;
|
|
48
|
+
toggle: () => void;
|
|
49
|
+
reset: () => void;
|
|
50
|
+
seek: (seconds: number) => void;
|
|
51
|
+
seekByPercent: (percent: number) => void;
|
|
52
|
+
setVolume: (volume: number) => void;
|
|
53
|
+
toggleMute: () => void;
|
|
54
|
+
size: NonNullable<AudioPlayerVariantProps["size"]>;
|
|
55
|
+
shape: NonNullable<AudioPlayerVariantProps["shape"]>;
|
|
56
|
+
appearance: NonNullable<AudioPlayerVariantProps["appearance"]>;
|
|
57
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiAudioPlayerAppearances,
|
|
5
|
+
zuiAudioPlayerBarBase,
|
|
6
|
+
zuiAudioPlayerBase,
|
|
7
|
+
zuiAudioPlayerShapes,
|
|
8
|
+
zuiAudioPlayerSizes,
|
|
9
|
+
zuiAudioPlayerTimeBase,
|
|
10
|
+
zuiAudioPlayerTrackBase,
|
|
11
|
+
zuiAudioPlayerTrackSizes,
|
|
12
|
+
} from "../../design-system/audio-player";
|
|
13
|
+
|
|
14
|
+
export const audioPlayerVariants = cva(
|
|
15
|
+
[...zuiAudioPlayerBase, "flex flex-col"],
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
appearance: zuiAudioPlayerAppearances,
|
|
19
|
+
size: zuiAudioPlayerSizes,
|
|
20
|
+
shape: zuiAudioPlayerShapes,
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
appearance: "default",
|
|
24
|
+
size: "md",
|
|
25
|
+
shape: "rounded",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const audioPlayerTrackVariants = cva([...zuiAudioPlayerTrackBase], {
|
|
31
|
+
variants: {
|
|
32
|
+
size: zuiAudioPlayerTrackSizes,
|
|
33
|
+
shape: zuiAudioPlayerShapes,
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
size: "md",
|
|
37
|
+
shape: "rounded",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const audioPlayerBarVariants = cva(zuiAudioPlayerBarBase);
|
|
42
|
+
|
|
43
|
+
export const audioPlayerTimeVariants = cva(zuiAudioPlayerTimeBase);
|
package/dist/chunk-D2GISTDL.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkNZSZE36T_js = require('./chunk-NZSZE36T.js');
|
|
4
|
-
var classVarianceAuthority = require('class-variance-authority');
|
|
5
|
-
|
|
6
|
-
var buttonVariants = classVarianceAuthority.cva(chunkNZSZE36T_js.zuiButtonBase, {
|
|
7
|
-
variants: {
|
|
8
|
-
appearance: chunkNZSZE36T_js.zuiButtonAppearances,
|
|
9
|
-
size: chunkNZSZE36T_js.zuiButtonSizes
|
|
10
|
-
},
|
|
11
|
-
defaultVariants: {
|
|
12
|
-
appearance: "default",
|
|
13
|
-
size: "md"
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
exports.buttonVariants = buttonVariants;
|
|
18
|
-
//# sourceMappingURL=chunk-D2GISTDL.js.map
|
|
19
|
-
//# sourceMappingURL=chunk-D2GISTDL.js.map
|