Interface web complète : formulaire d'ajout (URL directe ou recherche de chaîne) et panneau de file d'attente avec sondage de la progression. - views/downloadForm/ : sélecteur de mode (vidéo/audio/miniature) partagé, ajout direct par URL, recherche de chaîne (GET /api/channel) avec ajout par résultat. Construction DOM via createElement/textContent (jamais d'innerHTML avec du texte venant de YouTube), pas juste par prudence : ça évite aussi tout risque d'injection depuis un titre de vidéo. - views/queuePanel/ : sonde GET /api/queue toutes les 1,5s, affiche statut/ progression/erreur par élément, annulation et lien de téléchargement natif (<a download>, pas de fetch+blob). - api/ : clients fetch typés (channelApi, queueApi), chaque await dans son propre try/catch avec message d'erreur adapté (réseau vs HTTP vs JSON). - QueueItem gagne un champ title optionnel (purement informatif, jamais utilisé pour construire la commande yt-dlp) pour un affichage plus parlant qu'une URL brute. - client/tsconfig.json + build.mjs : cible ES2022 (nécessaire pour Error.cause, jusque-là ES2020). - Vitest ajouté côté client (fonctions pures de src/format.ts ; le câblage DOM est vérifié manuellement, pas de jsdom pour l'instant). Deux bugs réels trouvés et corrigés en testant dans un vrai navigateur (pas seulement en lisant le code) : - CSS : un attribut `hidden` sur un conteneur `display: flex` se faisait écraser par cette règle (même spécificité, la feuille de l'auteur gagne) - les deux panneaux d'options restaient visibles en même temps. - server/src/routes/channel.ts : ne mappait vers 502 que les YtDlpError (code de sortie non nul), pas les erreurs brutes de spawn (ex. binaire introuvable - ENOENT), qui remontaient donc en 500 générique. Corrigé + test de non-régression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import Fastify from 'fastify';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../src/services/ytDlpRunner.js', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../src/services/ytDlpRunner.js')>();
|
|
return { ...actual, runYtDlp: vi.fn() };
|
|
});
|
|
|
|
import { channelRoutes } from '../src/routes/channel.js';
|
|
import { YtDlpError, runYtDlp } from '../src/services/ytDlpRunner.js';
|
|
|
|
describe('GET /api/channel', () => {
|
|
it('retourne les vidéos correspondantes pour une chaîne valide', async () => {
|
|
vi.mocked(runYtDlp).mockResolvedValue(
|
|
JSON.stringify({ id: 'v1', title: 'Test video', url: 'https://youtu.be/v1' }),
|
|
);
|
|
|
|
const app = Fastify();
|
|
await app.register(channelRoutes);
|
|
|
|
const response = await app.inject({
|
|
method: 'GET',
|
|
url: '/api/channel?channel=@test§ion=videos',
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json()).toEqual({
|
|
videos: [
|
|
{
|
|
id: 'v1',
|
|
title: 'Test video',
|
|
url: 'https://youtu.be/v1',
|
|
thumbnailUrl: null,
|
|
durationSeconds: null,
|
|
uploadDate: null,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('renvoie 400 pour un identifiant de chaîne invalide', async () => {
|
|
const app = Fastify();
|
|
await app.register(channelRoutes);
|
|
|
|
const response = await app.inject({ method: 'GET', url: '/api/channel?channel=--exec' });
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
});
|
|
|
|
it('renvoie 400 si le paramètre channel est manquant (validation de schéma)', async () => {
|
|
const app = Fastify();
|
|
await app.register(channelRoutes);
|
|
|
|
const response = await app.inject({ method: 'GET', url: '/api/channel' });
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
});
|
|
|
|
it('renvoie 502 quand yt-dlp échoue avec un code de sortie non nul', async () => {
|
|
vi.mocked(runYtDlp).mockRejectedValue(new YtDlpError(1, 'network error'));
|
|
|
|
const app = Fastify();
|
|
await app.register(channelRoutes);
|
|
|
|
const response = await app.inject({ method: 'GET', url: '/api/channel?channel=@test' });
|
|
|
|
expect(response.statusCode).toBe(502);
|
|
});
|
|
|
|
it('renvoie 502 (pas 500) quand le binaire yt-dlp est introuvable (ENOENT)', async () => {
|
|
// Node rejette avec une Error brute (pas une YtDlpError) quand le spawn
|
|
// lui-même échoue - voir ytDlpRunner.ts, l'événement 'error' du child.
|
|
const spawnError = Object.assign(new Error('spawn yt-dlp ENOENT'), { code: 'ENOENT' });
|
|
vi.mocked(runYtDlp).mockRejectedValue(spawnError);
|
|
|
|
const app = Fastify();
|
|
await app.register(channelRoutes);
|
|
|
|
const response = await app.inject({ method: 'GET', url: '/api/channel?channel=@test' });
|
|
|
|
expect(response.statusCode).toBe(502);
|
|
});
|
|
});
|