A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: more audio tests

+359
+359
tests/components/engine/audio/test.ts
··· 205 205 206 206 expect(result).toBe(0.4); 207 207 }); 208 + 209 + // Sample audio tests 210 + 211 + it("state returns undefined for unknown audio id", async () => { 212 + const result = await testWeb(async () => { 213 + const mod = await import("~/components/engine/audio/element.js"); 214 + const { trackA } = await import("~/testing/sample/tracks.js"); 215 + const engine = new mod.CLASS(); 216 + document.body.append(engine); 217 + 218 + engine.supply({ 219 + audio: [{ 220 + id: "audio-a", 221 + url: "/testing/sample/audio.mp3", 222 + isPreload: false, 223 + track: trackA, 224 + }], 225 + }); 226 + 227 + return engine.state("no-such-id"); 228 + }); 229 + 230 + expect(result).toBeUndefined(); 231 + }); 232 + 233 + it("state has initial loadingState of loading before audio loads", async () => { 234 + const result = await testWeb(async () => { 235 + const mod = await import("~/components/engine/audio/element.js"); 236 + const { trackA } = await import("~/testing/sample/tracks.js"); 237 + const engine = new mod.CLASS(); 238 + document.body.append(engine); 239 + 240 + engine.supply({ 241 + audio: [{ 242 + id: "audio-a", 243 + url: "/testing/sample/audio.mp3", 244 + isPreload: false, 245 + track: trackA, 246 + }], 247 + }); 248 + 249 + const st = engine.state("audio-a"); 250 + return { 251 + loadingState: st?.loadingState(), 252 + currentTime: st?.currentTime(), 253 + isPlaying: st?.isPlaying(), 254 + hasEnded: st?.hasEnded(), 255 + }; 256 + }); 257 + 258 + expect(result.loadingState).toBe("loading"); 259 + expect(result.currentTime).toBe(0); 260 + expect(result.isPlaying).toBe(false); 261 + expect(result.hasEnded).toBe(false); 262 + }); 263 + 264 + it("loadingState becomes loaded after audio loads", async () => { 265 + const result = await testWeb(async () => { 266 + const mod = await import("~/components/engine/audio/element.js"); 267 + const { trackA } = await import("~/testing/sample/tracks.js"); 268 + const engine = new mod.CLASS(); 269 + document.body.append(engine); 270 + 271 + engine.supply({ 272 + audio: [{ 273 + id: "audio-a", 274 + url: "/testing/sample/audio.mp3", 275 + isPreload: false, 276 + track: trackA, 277 + }], 278 + }); 279 + 280 + const audioEl = engine.querySelector( 281 + 'de-audio-item[id="audio-a"]:not([preload]) audio', 282 + ) as HTMLAudioElement; 283 + 284 + await new Promise<void>((resolve, reject) => { 285 + const st = engine.state("audio-a"); 286 + if (st?.loadingState() === "loaded") { resolve(); return; } 287 + const timer = setTimeout( 288 + () => reject(new Error("timeout waiting for audio load")), 289 + 10000, 290 + ); 291 + const done = () => { clearTimeout(timer); resolve(); }; 292 + audioEl.addEventListener("canplay", done, { once: true }); 293 + audioEl.addEventListener("suspend", done, { once: true }); 294 + audioEl.addEventListener( 295 + "error", 296 + () => { clearTimeout(timer); reject(new Error("audio load error")); }, 297 + { once: true }, 298 + ); 299 + }); 300 + 301 + return engine.state("audio-a")?.loadingState(); 302 + }); 303 + 304 + expect(result).toBe("loaded"); 305 + }); 306 + 307 + it("duration is positive after audio loads", async () => { 308 + const result = await testWeb(async () => { 309 + const mod = await import("~/components/engine/audio/element.js"); 310 + const { trackA } = await import("~/testing/sample/tracks.js"); 311 + const engine = new mod.CLASS(); 312 + document.body.append(engine); 313 + 314 + engine.supply({ 315 + audio: [{ 316 + id: "audio-a", 317 + url: "/testing/sample/audio.mp3", 318 + isPreload: false, 319 + track: trackA, 320 + }], 321 + }); 322 + 323 + const audioEl = engine.querySelector( 324 + 'de-audio-item[id="audio-a"]:not([preload]) audio', 325 + ) as HTMLAudioElement; 326 + 327 + await new Promise<void>((resolve, reject) => { 328 + if (audioEl.duration > 0 && isFinite(audioEl.duration)) { 329 + resolve(); 330 + return; 331 + } 332 + const timer = setTimeout( 333 + () => reject(new Error("timeout waiting for duration")), 334 + 10000, 335 + ); 336 + const done = () => { clearTimeout(timer); resolve(); }; 337 + audioEl.addEventListener("durationchange", done, { once: true }); 338 + audioEl.addEventListener( 339 + "error", 340 + () => { clearTimeout(timer); reject(new Error("audio load error")); }, 341 + { once: true }, 342 + ); 343 + }); 344 + 345 + return engine.state("audio-a")?.duration(); 346 + }); 347 + 348 + expect(result).toBeGreaterThan(0); 349 + }); 350 + 351 + it("currentTime is 0 before seek", async () => { 352 + const result = await testWeb(async () => { 353 + const mod = await import("~/components/engine/audio/element.js"); 354 + const { trackA } = await import("~/testing/sample/tracks.js"); 355 + const engine = new mod.CLASS(); 356 + document.body.append(engine); 357 + 358 + engine.supply({ 359 + audio: [{ 360 + id: "audio-a", 361 + url: "/testing/sample/audio.mp3", 362 + isPreload: false, 363 + track: trackA, 364 + }], 365 + }); 366 + 367 + const audioEl = engine.querySelector( 368 + 'de-audio-item[id="audio-a"]:not([preload]) audio', 369 + ) as HTMLAudioElement; 370 + 371 + await new Promise<void>((resolve, reject) => { 372 + const st = engine.state("audio-a"); 373 + if (st?.loadingState() === "loaded") { resolve(); return; } 374 + const timer = setTimeout( 375 + () => reject(new Error("timeout")), 376 + 10000, 377 + ); 378 + const done = () => { clearTimeout(timer); resolve(); }; 379 + audioEl.addEventListener("canplay", done, { once: true }); 380 + audioEl.addEventListener("suspend", done, { once: true }); 381 + audioEl.addEventListener( 382 + "error", 383 + () => { clearTimeout(timer); reject(new Error("audio load error")); }, 384 + { once: true }, 385 + ); 386 + }); 387 + 388 + return engine.state("audio-a")?.currentTime(); 389 + }); 390 + 391 + expect(result).toBe(0); 392 + }); 393 + 394 + it("seek by currentTime updates audio element currentTime", async () => { 395 + const result = await testWeb(async () => { 396 + const mod = await import("~/components/engine/audio/element.js"); 397 + const { trackA } = await import("~/testing/sample/tracks.js"); 398 + const engine = new mod.CLASS(); 399 + document.body.append(engine); 400 + 401 + engine.supply({ 402 + audio: [{ 403 + id: "audio-a", 404 + url: "/testing/sample/audio.mp3", 405 + isPreload: false, 406 + track: trackA, 407 + }], 408 + }); 409 + 410 + const audioEl = engine.querySelector( 411 + 'de-audio-item[id="audio-a"]:not([preload]) audio', 412 + ) as HTMLAudioElement; 413 + 414 + await new Promise<void>((resolve, reject) => { 415 + const st = engine.state("audio-a"); 416 + if (st?.loadingState() === "loaded") { resolve(); return; } 417 + const timer = setTimeout(() => reject(new Error("timeout")), 10000); 418 + const done = () => { clearTimeout(timer); resolve(); }; 419 + audioEl.addEventListener("canplay", done, { once: true }); 420 + audioEl.addEventListener("suspend", done, { once: true }); 421 + audioEl.addEventListener( 422 + "error", 423 + () => { clearTimeout(timer); reject(new Error("audio load error")); }, 424 + { once: true }, 425 + ); 426 + }); 427 + 428 + engine.seek({ audioId: "audio-a", currentTime: 1 }); 429 + 430 + return audioEl.currentTime; 431 + }); 432 + 433 + expect(result).toBe(1); 434 + }); 435 + 436 + it("seek by percentage updates audio element currentTime proportionally", async () => { 437 + const result = await testWeb(async () => { 438 + const mod = await import("~/components/engine/audio/element.js"); 439 + const { trackA } = await import("~/testing/sample/tracks.js"); 440 + const engine = new mod.CLASS(); 441 + document.body.append(engine); 442 + 443 + engine.supply({ 444 + audio: [{ 445 + id: "audio-a", 446 + url: "/testing/sample/audio.mp3", 447 + isPreload: false, 448 + track: trackA, 449 + }], 450 + }); 451 + 452 + const audioEl = engine.querySelector( 453 + 'de-audio-item[id="audio-a"]:not([preload]) audio', 454 + ) as HTMLAudioElement; 455 + 456 + await new Promise<void>((resolve, reject) => { 457 + if (audioEl.duration > 0 && isFinite(audioEl.duration)) { resolve(); return; } 458 + const timer = setTimeout(() => reject(new Error("timeout")), 10000); 459 + const done = () => { clearTimeout(timer); resolve(); }; 460 + audioEl.addEventListener("durationchange", done, { once: true }); 461 + audioEl.addEventListener( 462 + "error", 463 + () => { clearTimeout(timer); reject(new Error("audio load error")); }, 464 + { once: true }, 465 + ); 466 + }); 467 + 468 + engine.seek({ audioId: "audio-a", percentage: 0.5 }); 469 + 470 + return { 471 + currentTime: audioEl.currentTime, 472 + expected: 0.5 * audioEl.duration, 473 + }; 474 + }); 475 + 476 + expect(result.currentTime).toBe(result.expected); 477 + }); 478 + 479 + it("play sets isPlaying to true", async () => { 480 + const result = await testWeb(async () => { 481 + const mod = await import("~/components/engine/audio/element.js"); 482 + const { trackA } = await import("~/testing/sample/tracks.js"); 483 + const engine = new mod.CLASS(); 484 + document.body.append(engine); 485 + 486 + engine.supply({ 487 + audio: [{ 488 + id: "audio-a", 489 + url: "/testing/sample/audio.mp3", 490 + isPreload: false, 491 + track: trackA, 492 + }], 493 + }); 494 + 495 + const audioEl = engine.querySelector( 496 + 'de-audio-item[id="audio-a"]:not([preload]) audio', 497 + ) as HTMLAudioElement; 498 + 499 + await new Promise<void>((resolve, reject) => { 500 + const st = engine.state("audio-a"); 501 + if (st?.loadingState() === "loaded") { resolve(); return; } 502 + const timer = setTimeout(() => reject(new Error("timeout")), 10000); 503 + const done = () => { clearTimeout(timer); resolve(); }; 504 + audioEl.addEventListener("canplay", done, { once: true }); 505 + audioEl.addEventListener("suspend", done, { once: true }); 506 + audioEl.addEventListener( 507 + "error", 508 + () => { clearTimeout(timer); reject(new Error("audio load error")); }, 509 + { once: true }, 510 + ); 511 + }); 512 + 513 + // play() sets isPlaying optimistically before the audio.play() promise settles 514 + engine.play({ audioId: "audio-a", volume: 0.5 }); 515 + 516 + return engine.state("audio-a")?.isPlaying(); 517 + }); 518 + 519 + expect(result).toBe(true); 520 + }); 521 + 522 + it("pause resets isPlaying to false", async () => { 523 + const result = await testWeb(async () => { 524 + const mod = await import("~/components/engine/audio/element.js"); 525 + const { trackA } = await import("~/testing/sample/tracks.js"); 526 + const engine = new mod.CLASS(); 527 + document.body.append(engine); 528 + 529 + engine.supply({ 530 + audio: [{ 531 + id: "audio-a", 532 + url: "/testing/sample/audio.mp3", 533 + isPreload: false, 534 + track: trackA, 535 + }], 536 + }); 537 + 538 + const audioEl = engine.querySelector( 539 + 'de-audio-item[id="audio-a"]:not([preload]) audio', 540 + ) as HTMLAudioElement; 541 + 542 + await new Promise<void>((resolve, reject) => { 543 + const st = engine.state("audio-a"); 544 + if (st?.loadingState() === "loaded") { resolve(); return; } 545 + const timer = setTimeout(() => reject(new Error("timeout")), 10000); 546 + const done = () => { clearTimeout(timer); resolve(); }; 547 + audioEl.addEventListener("canplay", done, { once: true }); 548 + audioEl.addEventListener("suspend", done, { once: true }); 549 + audioEl.addEventListener( 550 + "error", 551 + () => { clearTimeout(timer); reject(new Error("audio load error")); }, 552 + { once: true }, 553 + ); 554 + }); 555 + 556 + engine.play({ audioId: "audio-a", volume: 0.5 }); 557 + engine.pause({ audioId: "audio-a" }); 558 + 559 + // Wait for pause event to propagate 560 + await new Promise<void>((resolve) => setTimeout(resolve, 50)); 561 + 562 + return engine.state("audio-a")?.isPlaying(); 563 + }); 564 + 565 + expect(result).toBe(false); 566 + }); 208 567 });